Skip to content

fix(synology-chat): prevent restart loop in startAccount#23074

Merged
steipete merged 4 commits intoopenclaw:mainfrom
druide67:fix/synology-chat-restart-loop
Mar 2, 2026
Merged

fix(synology-chat): prevent restart loop in startAccount#23074
steipete merged 4 commits intoopenclaw:mainfrom
druide67:fix/synology-chat-restart-loop

Conversation

@druide67
Copy link
Contributor

@druide67 druide67 commented Feb 22, 2026

Summary

  • Problem: The Synology Chat channel plugin enters a restart loop (exponential backoff, up to 10 attempts) immediately after starting
  • Root cause: startAccount returns a synchronous { stop } object. The gateway wraps the return value in Promise.resolve(), which resolves immediately. The gateway interprets this as a crash and triggers auto-restart.
  • Fix: Replace the synchronous return with a Promise<void> that stays pending until ctx.abortSignal fires, keeping the channel alive until the gateway explicitly requests shutdown.

Change Type

  • Bug fix

Scope

  • Integrations (synology-chat extension only)

What Changed

  • extensions/synology-chat/src/channel.ts: gateway.startAccount now returns a pending Promise that resolves on abortSignal, instead of a synchronous { stop } object

Security Impact

  • No security impact — only changes the lifecycle management of the channel plugin

Repro + Verification

  1. Start OpenClaw with Synology Chat enabled
  2. Before fix: gateway logs show auto-restart attempt 1/10, 2/10, etc. with exponential backoff
  3. After fix: single startup log, no restart attempts

Human Verification

  • Tested on Synology DS923+ with DSM 7.2
  • Single startup, no restart loop in logs
  • Bot responds correctly to DMs
  • Clean shutdown on abortSignal

Compatibility

  • Backward compatible: yes
  • No config changes needed

🤖 Generated with Claude Code

Greptile Summary

Fixed restart loop in Synology Chat channel by returning a pending Promise from startAccount instead of a synchronous object. The gateway wraps all returns in Promise.resolve(), so the old synchronous { stop } return resolved immediately, triggering exponential backoff restarts.

Critical Issues:

  • Incomplete fix: disabled/unconfigured accounts (lines 213, 220) still return synchronous { stop: () => {} }, which will cause the same restart loop
  • Tests are now incorrect: they expect { stop: function } but the fix changes the return type to a pending Promise

What works:

  • Properly configured accounts now return a pending Promise that only resolves when abortSignal fires
  • Cleanup logic properly unregisters HTTP routes and removes event listeners
  • Fix aligns with the gateway's expectation that startAccount returns a Promise that stays pending while the channel runs

Confidence Score: 2/5

  • This PR has critical logical errors that will cause the same restart loop issue it claims to fix
  • The fix is incomplete and partially incorrect. While it correctly addresses the restart loop for properly configured accounts (lines 287-299), it fails to apply the same fix to disabled/unconfigured accounts (lines 213, 220), which still return the synchronous { stop: () => {} } that causes restart loops. Additionally, the tests are now incorrect and would fail to catch the incomplete fix since they still expect the old return type.
  • extensions/synology-chat/src/channel.ts (lines 213, 220 must return pending Promise), extensions/synology-chat/src/channel.test.ts (tests expect wrong return type)

Last reviewed commit: ded2228

(3/5) Reply to the agent's comments like "Can you suggest a fix for this @greptileai?" or ask follow-up questions!

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

16 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +211 to +214
if (!account.enabled) {
log?.info?.(`Synology Chat account ${accountId} is disabled, skipping`);
return { stop: () => {} };
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

returning synchronous { stop: () => {} } will cause the same restart loop for disabled accounts

the gateway wraps all startAccount returns in Promise.resolve(), so returning { stop: () => {} } resolves immediately and triggers auto-restart logic (src/gateway/server-channels.ts:203-228)

Suggested change
if (!account.enabled) {
log?.info?.(`Synology Chat account ${accountId} is disabled, skipping`);
return { stop: () => {} };
}
log?.info?.(`Synology Chat account ${accountId} is disabled, skipping`);
return new Promise<void>(() => {});
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/synology-chat/src/channel.ts
Line: 211-214

Comment:
returning synchronous `{ stop: () => {} }` will cause the same restart loop for disabled accounts

the gateway wraps all `startAccount` returns in `Promise.resolve()`, so returning `{ stop: () => {} }` resolves immediately and triggers auto-restart logic (src/gateway/server-channels.ts:203-228)

```suggestion
          log?.info?.(`Synology Chat account ${accountId} is disabled, skipping`);
          return new Promise<void>(() => {});
        }
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +216 to +221
if (!account.token || !account.incomingUrl) {
log?.warn?.(
`Synology Chat account ${accountId} not fully configured (missing token or incomingUrl)`,
);
return { stop: () => {} };
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same restart loop issue for unconfigured accounts

Suggested change
if (!account.token || !account.incomingUrl) {
log?.warn?.(
`Synology Chat account ${accountId} not fully configured (missing token or incomingUrl)`,
);
return { stop: () => {} };
}
log?.warn?.(
`Synology Chat account ${accountId} not fully configured (missing token or incomingUrl)`,
);
return new Promise<void>(() => {});
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/synology-chat/src/channel.ts
Line: 216-221

Comment:
same restart loop issue for unconfigured accounts

```suggestion
          log?.warn?.(
            `Synology Chat account ${accountId} not fully configured (missing token or incomingUrl)`,
          );
          return new Promise<void>(() => {});
        }
```

How can I resolve this? If you propose a fix, please make it concise.

log: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
};
const result = await plugin.gateway.startAccount(ctx);
expect(typeof result.stop).toBe("function");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test expects { stop: function } but the fix changes this to return a pending Promise

after the fix, disabled accounts should return new Promise<void>(() => {}) (never-resolving promise), not { stop: () => {} }

update test to verify the return is a Promise that stays pending

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/synology-chat/src/channel.test.ts
Line: 323

Comment:
test expects `{ stop: function }` but the fix changes this to return a pending Promise

after the fix, disabled accounts should return `new Promise<void>(() => {})` (never-resolving promise), not `{ stop: () => {} }`

update test to verify the return is a Promise that stays pending

How can I resolve this? If you propose a fix, please make it concise.

log: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
};
const result = await plugin.gateway.startAccount(ctx);
expect(typeof result.stop).toBe("function");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same issue - test expects { stop: function } but should expect a pending Promise

Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/synology-chat/src/channel.test.ts
Line: 336

Comment:
same issue - test expects `{ stop: function }` but should expect a pending Promise

How can I resolve this? If you propose a fix, please make it concise.

@druide67 druide67 force-pushed the fix/synology-chat-restart-loop branch from ded2228 to 102b919 Compare February 23, 2026 06:33
@druide67 druide67 force-pushed the fix/synology-chat-restart-loop branch 2 times, most recently from 07442e9 to 005e1a2 Compare February 24, 2026 12:07
@druide67
Copy link
Contributor Author

Rebased on latest main and addressed all Greptile review comments:

  • Disabled/unconfigured accounts now return new Promise<void>(() => {}) (never-resolving) instead of { stop: () => {} }, preventing the restart loop triggered by Promise.resolve() in the gateway's startAccount wrapper.
  • Cleanup function now includes both activeRouteUnregisters.delete(routeKey) and ctx.abortSignal?.removeEventListener("abort", cleanup).
  • Tests updated: pending-promise race pattern for disabled/unconfigured accounts, AbortController for the stale-route deregistration test.

All 22 tests pass locally (vitest 4.0.18, 525ms).

@druide67 druide67 force-pushed the fix/synology-chat-restart-loop branch from 005e1a2 to ae2d828 Compare February 24, 2026 12:20
@druide67
Copy link
Contributor Author

Friendly ping — this PR has been open for a few days now. CI is fully green, mergeable, and Greptile review comments have been addressed (see my previous comment). Would appreciate a maintainer review when you get a chance. Happy to address any feedback. Thanks!

druide67 and others added 4 commits March 2, 2026 20:04
startAccount must return a Promise that stays pending while the channel
is running. The gateway wraps the return value in Promise.resolve(), and
when it resolves, the gateway thinks the channel crashed and auto-restarts
with exponential backoff (5s → 10s → 20s..., up to 10 attempts).

Replace the synchronous { stop } return with a Promise<void> that resolves
only when ctx.abortSignal fires, keeping the channel alive until shutdown.

Tested on Synology DS923+ with DSM 7.2 — single startup, no restart loop.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
startAccount returns `void | { stop: () => void }` — TypeScript requires
a type guard before accessing .stop on the union type. Added proper checks
in both integration and unit tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ws compat

Replace EventEmitter + process.nextTick with Readable stream for
request body simulation. The process.nextTick approach caused the test
to hang on Windows CI (120s timeout) because events were not reliably
delivered to readBody() listeners.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@steipete steipete force-pushed the fix/synology-chat-restart-loop branch from 65dc370 to 36ddef7 Compare March 2, 2026 20:06
@steipete steipete merged commit b52561b into openclaw:main Mar 2, 2026
9 checks passed
@steipete
Copy link
Contributor

steipete commented Mar 2, 2026

Landed via temp rebase onto main.

  • Gate: pnpm vitest run extensions/synology-chat/src/channel.test.ts extensions/synology-chat/src/channel.integration.test.ts --maxWorkers=1
  • Land commit: 36ddef7
  • Merge commit: b52561b

Thanks @druide67!

execute008 pushed a commit to execute008/openclaw that referenced this pull request Mar 2, 2026
)

* fix(synology-chat): prevent restart loop in startAccount

startAccount must return a Promise that stays pending while the channel
is running. The gateway wraps the return value in Promise.resolve(), and
when it resolves, the gateway thinks the channel crashed and auto-restarts
with exponential backoff (5s → 10s → 20s..., up to 10 attempts).

Replace the synchronous { stop } return with a Promise<void> that resolves
only when ctx.abortSignal fires, keeping the channel alive until shutdown.

Tested on Synology DS923+ with DSM 7.2 — single startup, no restart loop.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(synology-chat): add type guards for startAccount return value

startAccount returns `void | { stop: () => void }` — TypeScript requires
a type guard before accessing .stop on the union type. Added proper checks
in both integration and unit tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(synology-chat): use Readable stream in integration test for Windows compat

Replace EventEmitter + process.nextTick with Readable stream for
request body simulation. The process.nextTick approach caused the test
to hang on Windows CI (120s timeout) because events were not reliably
delivered to readBody() listeners.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: stabilize synology gateway account lifecycle (openclaw#23074) (thanks @druide67)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
dawi369 pushed a commit to dawi369/davis that referenced this pull request Mar 3, 2026
)

* fix(synology-chat): prevent restart loop in startAccount

startAccount must return a Promise that stays pending while the channel
is running. The gateway wraps the return value in Promise.resolve(), and
when it resolves, the gateway thinks the channel crashed and auto-restarts
with exponential backoff (5s → 10s → 20s..., up to 10 attempts).

Replace the synchronous { stop } return with a Promise<void> that resolves
only when ctx.abortSignal fires, keeping the channel alive until shutdown.

Tested on Synology DS923+ with DSM 7.2 — single startup, no restart loop.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(synology-chat): add type guards for startAccount return value

startAccount returns `void | { stop: () => void }` — TypeScript requires
a type guard before accessing .stop on the union type. Added proper checks
in both integration and unit tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(synology-chat): use Readable stream in integration test for Windows compat

Replace EventEmitter + process.nextTick with Readable stream for
request body simulation. The process.nextTick approach caused the test
to hang on Windows CI (120s timeout) because events were not reliably
delivered to readBody() listeners.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: stabilize synology gateway account lifecycle (openclaw#23074) (thanks @druide67)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
OWALabuy pushed a commit to kcinzgg/openclaw that referenced this pull request Mar 4, 2026
)

* fix(synology-chat): prevent restart loop in startAccount

startAccount must return a Promise that stays pending while the channel
is running. The gateway wraps the return value in Promise.resolve(), and
when it resolves, the gateway thinks the channel crashed and auto-restarts
with exponential backoff (5s → 10s → 20s..., up to 10 attempts).

Replace the synchronous { stop } return with a Promise<void> that resolves
only when ctx.abortSignal fires, keeping the channel alive until shutdown.

Tested on Synology DS923+ with DSM 7.2 — single startup, no restart loop.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(synology-chat): add type guards for startAccount return value

startAccount returns `void | { stop: () => void }` — TypeScript requires
a type guard before accessing .stop on the union type. Added proper checks
in both integration and unit tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(synology-chat): use Readable stream in integration test for Windows compat

Replace EventEmitter + process.nextTick with Readable stream for
request body simulation. The process.nextTick approach caused the test
to hang on Windows CI (120s timeout) because events were not reliably
delivered to readBody() listeners.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: stabilize synology gateway account lifecycle (openclaw#23074) (thanks @druide67)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
zooqueen pushed a commit to hanzoai/bot that referenced this pull request Mar 6, 2026
)

* fix(synology-chat): prevent restart loop in startAccount

startAccount must return a Promise that stays pending while the channel
is running. The gateway wraps the return value in Promise.resolve(), and
when it resolves, the gateway thinks the channel crashed and auto-restarts
with exponential backoff (5s → 10s → 20s..., up to 10 attempts).

Replace the synchronous { stop } return with a Promise<void> that resolves
only when ctx.abortSignal fires, keeping the channel alive until shutdown.

Tested on Synology DS923+ with DSM 7.2 — single startup, no restart loop.


* fix(synology-chat): add type guards for startAccount return value

startAccount returns `void | { stop: () => void }` — TypeScript requires
a type guard before accessing .stop on the union type. Added proper checks
in both integration and unit tests.


* fix(synology-chat): use Readable stream in integration test for Windows compat

Replace EventEmitter + process.nextTick with Readable stream for
request body simulation. The process.nextTick approach caused the test
to hang on Windows CI (120s timeout) because events were not reliably
delivered to readBody() listeners.


* fix: stabilize synology gateway account lifecycle (openclaw#23074) (thanks @druide67)

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants