opensquid 0.5.441 → 0.5.447
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/functions/arm_scope.d.ts +27 -0
- package/dist/functions/arm_scope.d.ts.map +1 -0
- package/dist/functions/arm_scope.js +52 -0
- package/dist/functions/arm_scope.js.map +1 -0
- package/dist/functions/index.d.ts +1 -0
- package/dist/functions/index.d.ts.map +1 -1
- package/dist/functions/index.js +1 -0
- package/dist/functions/index.js.map +1 -1
- package/dist/runtime/bootstrap.d.ts.map +1 -1
- package/dist/runtime/bootstrap.js +2 -0
- package/dist/runtime/bootstrap.js.map +1 -1
- package/dist/runtime/handoff/render.d.ts +5 -4
- package/dist/runtime/handoff/render.d.ts.map +1 -1
- package/dist/runtime/handoff/render.js +7 -7
- package/dist/runtime/handoff/render.js.map +1 -1
- package/dist/runtime/hooks/active_task_mirror.js +0 -0
- package/dist/runtime/hooks/apply_patch.js +0 -0
- package/dist/runtime/hooks/dispatch.js +0 -0
- package/dist/runtime/hooks/hook_output.js +0 -0
- package/dist/runtime/hooks/memory_reconcile.js +0 -0
- package/dist/runtime/hooks/new_project_detect.js +0 -0
- package/dist/runtime/hooks/profession_resolver.js +0 -0
- package/dist/runtime/hooks/scope_intent.js +0 -0
- package/dist/runtime/hooks/session_id.js +0 -0
- package/dist/runtime/hooks/session_liveness.js +0 -0
- package/dist/runtime/hooks/stop_drive.js +0 -0
- package/dist/runtime/hooks/stop_stream.js +0 -0
- package/dist/runtime/hooks/subagent_guard.js +0 -0
- package/dist/runtime/hooks/transcript.js +0 -0
- package/dist/runtime/hooks/transcript_tasks.js +0 -0
- package/dist/runtime/ralph/orchestrator.d.ts.map +1 -1
- package/dist/runtime/ralph/orchestrator.js +2 -1
- package/dist/runtime/ralph/orchestrator.js.map +1 -1
- package/dist/setup/cli/limits_state.d.ts.map +1 -1
- package/dist/setup/cli/limits_state.js +6 -40
- package/dist/setup/cli/limits_state.js.map +1 -1
- package/dist/setup/cli/pack_walk.d.ts +32 -0
- package/dist/setup/cli/pack_walk.d.ts.map +1 -0
- package/dist/setup/cli/pack_walk.js +76 -0
- package/dist/setup/cli/pack_walk.js.map +1 -0
- package/dist/setup/cli/permissions_state.d.ts.map +1 -1
- package/dist/setup/cli/permissions_state.js +6 -37
- package/dist/setup/cli/permissions_state.js.map +1 -1
- package/dist/setup/cli/triggers_state.d.ts.map +1 -1
- package/dist/setup/cli/triggers_state.js +3 -29
- package/dist/setup/cli/triggers_state.js.map +1 -1
- package/dist/workgraph/events.d.ts.map +1 -1
- package/dist/workgraph/events.js +10 -0
- package/dist/workgraph/events.js.map +1 -1
- package/dist/workgraph/store.d.ts.map +1 -1
- package/dist/workgraph/store.js +5 -0
- package/dist/workgraph/store.js.map +1 -1
- package/dist/workgraph/types.d.ts +2 -1
- package/dist/workgraph/types.d.ts.map +1 -1
- package/docs/ARCHITECTURE.md +268 -0
- package/package.json +5 -3
- package/packs/builtin/coding-flow/skills/entry-and-handoffs/skill.yaml +13 -17
- package/dist/anti-drift/evaluator.d.ts +0 -88
- package/dist/anti-drift/evaluator.d.ts.map +0 -1
- package/dist/anti-drift/evaluator.js +0 -417
- package/dist/anti-drift/evaluator.js.map +0 -1
- package/dist/anti-drift/evaluator.test.js +0 -78
- package/dist/anti-drift/rules.d.ts +0 -80
- package/dist/anti-drift/rules.d.ts.map +0 -1
- package/dist/anti-drift/rules.js +0 -368
- package/dist/anti-drift/rules.js.map +0 -1
- package/dist/anti-drift/rules.test.js +0 -213
- package/dist/anti-drift/state.d.ts +0 -107
- package/dist/anti-drift/state.d.ts.map +0 -1
- package/dist/anti-drift/state.js +0 -177
- package/dist/anti-drift/state.js.map +0 -1
- package/dist/anti-drift/state.test.js +0 -120
- package/dist/chat/adapters/discord.d.ts +0 -41
- package/dist/chat/adapters/discord.d.ts.map +0 -1
- package/dist/chat/adapters/discord.js +0 -176
- package/dist/chat/adapters/discord.js.map +0 -1
- package/dist/chat/adapters/discord.test.js +0 -25
- package/dist/chat/adapters/slack.d.ts +0 -43
- package/dist/chat/adapters/slack.d.ts.map +0 -1
- package/dist/chat/adapters/slack.js +0 -172
- package/dist/chat/adapters/slack.js.map +0 -1
- package/dist/chat/adapters/slack.test.js +0 -30
- package/dist/chat/adapters/telegram.d.ts +0 -148
- package/dist/chat/adapters/telegram.d.ts.map +0 -1
- package/dist/chat/adapters/telegram.js +0 -498
- package/dist/chat/adapters/telegram.js.map +0 -1
- package/dist/chat/adapters/telegram.test.js +0 -94
- package/dist/chat/config.d.ts +0 -98
- package/dist/chat/config.d.ts.map +0 -1
- package/dist/chat/config.js +0 -185
- package/dist/chat/config.js.map +0 -1
- package/dist/chat/daemon/active-project.d.ts +0 -17
- package/dist/chat/daemon/active-project.d.ts.map +0 -1
- package/dist/chat/daemon/active-project.js +0 -23
- package/dist/chat/daemon/active-project.js.map +0 -1
- package/dist/chat/daemon/autospawn.d.ts +0 -40
- package/dist/chat/daemon/autospawn.d.ts.map +0 -1
- package/dist/chat/daemon/autospawn.js +0 -129
- package/dist/chat/daemon/autospawn.js.map +0 -1
- package/dist/chat/daemon/autospawn.test.js +0 -112
- package/dist/chat/daemon/cli.d.ts +0 -18
- package/dist/chat/daemon/cli.d.ts.map +0 -1
- package/dist/chat/daemon/cli.js +0 -71
- package/dist/chat/daemon/cli.js.map +0 -1
- package/dist/chat/daemon/collisions.js +0 -384
- package/dist/chat/daemon/health-check.d.ts +0 -69
- package/dist/chat/daemon/health-check.d.ts.map +0 -1
- package/dist/chat/daemon/health-check.js +0 -112
- package/dist/chat/daemon/health-check.js.map +0 -1
- package/dist/chat/daemon/inbox-read.d.ts +0 -35
- package/dist/chat/daemon/inbox-read.d.ts.map +0 -1
- package/dist/chat/daemon/inbox-read.js +0 -75
- package/dist/chat/daemon/inbox-read.js.map +0 -1
- package/dist/chat/daemon/inbox-read.test.js +0 -97
- package/dist/chat/daemon/inbox.d.ts +0 -63
- package/dist/chat/daemon/inbox.d.ts.map +0 -1
- package/dist/chat/daemon/inbox.js +0 -56
- package/dist/chat/daemon/inbox.js.map +0 -1
- package/dist/chat/daemon/inbox.test.js +0 -110
- package/dist/chat/daemon/lifecycle.d.ts +0 -71
- package/dist/chat/daemon/lifecycle.d.ts.map +0 -1
- package/dist/chat/daemon/lifecycle.js +0 -221
- package/dist/chat/daemon/lifecycle.js.map +0 -1
- package/dist/chat/daemon/lifecycle.test.js +0 -163
- package/dist/chat/daemon/protocol.d.ts +0 -107
- package/dist/chat/daemon/protocol.d.ts.map +0 -1
- package/dist/chat/daemon/protocol.js +0 -54
- package/dist/chat/daemon/protocol.js.map +0 -1
- package/dist/chat/daemon/routing.d.ts +0 -140
- package/dist/chat/daemon/routing.d.ts.map +0 -1
- package/dist/chat/daemon/routing.js +0 -198
- package/dist/chat/daemon/routing.js.map +0 -1
- package/dist/chat/daemon/routing.test.js +0 -259
- package/dist/chat/daemon/rpc-client.d.ts +0 -45
- package/dist/chat/daemon/rpc-client.d.ts.map +0 -1
- package/dist/chat/daemon/rpc-client.js +0 -133
- package/dist/chat/daemon/rpc-client.js.map +0 -1
- package/dist/chat/daemon/rpc-server.d.ts +0 -39
- package/dist/chat/daemon/rpc-server.d.ts.map +0 -1
- package/dist/chat/daemon/rpc-server.js +0 -385
- package/dist/chat/daemon/rpc-server.js.map +0 -1
- package/dist/chat/daemon/rpc.test.js +0 -177
- package/dist/chat/daemon/subscribers.js +0 -257
- package/dist/chat/daemon/worker.d.ts +0 -27
- package/dist/chat/daemon/worker.d.ts.map +0 -1
- package/dist/chat/daemon/worker.js +0 -313
- package/dist/chat/daemon/worker.js.map +0 -1
- package/dist/chat/daemon/workspace-topic.js +0 -324
- package/dist/chat/env-token.d.ts +0 -60
- package/dist/chat/env-token.d.ts.map +0 -1
- package/dist/chat/env-token.js +0 -137
- package/dist/chat/env-token.js.map +0 -1
- package/dist/chat/env-token.test.js +0 -160
- package/dist/chat/factory.d.ts +0 -30
- package/dist/chat/factory.d.ts.map +0 -1
- package/dist/chat/factory.js +0 -50
- package/dist/chat/factory.js.map +0 -1
- package/dist/chat/factory.test.js +0 -55
- package/dist/chat/gateway.d.ts +0 -176
- package/dist/chat/gateway.d.ts.map +0 -1
- package/dist/chat/gateway.js +0 -146
- package/dist/chat/gateway.js.map +0 -1
- package/dist/chat/gateway.test.js +0 -192
- package/dist/claude-md.d.ts +0 -39
- package/dist/claude-md.d.ts.map +0 -1
- package/dist/claude-md.js +0 -113
- package/dist/claude-md.js.map +0 -1
- package/dist/claude-md.test.js +0 -91
- package/dist/codex/activate.d.ts +0 -66
- package/dist/codex/activate.d.ts.map +0 -1
- package/dist/codex/activate.js +0 -329
- package/dist/codex/activate.js.map +0 -1
- package/dist/codex/activate.test.js +0 -229
- package/dist/codex/bundled-default/bundled-default.test.js +0 -161
- package/dist/codex/cli-publish.test.js +0 -133
- package/dist/codex/cli.d.ts +0 -35
- package/dist/codex/cli.d.ts.map +0 -1
- package/dist/codex/cli.js +0 -554
- package/dist/codex/cli.js.map +0 -1
- package/dist/codex/cli.test.js +0 -277
- package/dist/codex/import-skill-md.d.ts +0 -53
- package/dist/codex/import-skill-md.d.ts.map +0 -1
- package/dist/codex/import-skill-md.js +0 -236
- package/dist/codex/import-skill-md.js.map +0 -1
- package/dist/codex/import-skill-md.test.js +0 -225
- package/dist/codex/loader.d.ts +0 -27
- package/dist/codex/loader.d.ts.map +0 -1
- package/dist/codex/loader.js +0 -86
- package/dist/codex/loader.js.map +0 -1
- package/dist/codex/loader.test.js +0 -75
- package/dist/codex/parse.d.ts +0 -28
- package/dist/codex/parse.d.ts.map +0 -1
- package/dist/codex/parse.js +0 -309
- package/dist/codex/parse.js.map +0 -1
- package/dist/codex/parse.test.js +0 -241
- package/dist/codex/store.d.ts +0 -87
- package/dist/codex/store.d.ts.map +0 -1
- package/dist/codex/store.js +0 -205
- package/dist/codex/store.js.map +0 -1
- package/dist/codex/store.test.js +0 -242
- package/dist/codex/types.d.ts +0 -398
- package/dist/codex/types.d.ts.map +0 -1
- package/dist/codex/types.js +0 -21
- package/dist/codex/types.js.map +0 -1
- package/dist/config.d.ts +0 -53
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js +0 -202
- package/dist/config.js.map +0 -1
- package/dist/config.test.js +0 -117
- package/dist/engine/cli.d.ts +0 -14
- package/dist/engine/cli.d.ts.map +0 -1
- package/dist/engine/cli.js +0 -171
- package/dist/engine/cli.js.map +0 -1
- package/dist/engine/client.d.ts +0 -219
- package/dist/engine/client.d.ts.map +0 -1
- package/dist/engine/client.js +0 -312
- package/dist/engine/client.js.map +0 -1
- package/dist/engine/config.d.ts +0 -62
- package/dist/engine/config.d.ts.map +0 -1
- package/dist/engine/config.js +0 -223
- package/dist/engine/config.js.map +0 -1
- package/dist/engine/index.d.ts +0 -17
- package/dist/engine/index.d.ts.map +0 -1
- package/dist/engine/index.js +0 -16
- package/dist/engine/index.js.map +0 -1
- package/dist/engine/resolver.d.ts +0 -62
- package/dist/engine/resolver.d.ts.map +0 -1
- package/dist/engine/resolver.js +0 -103
- package/dist/engine/resolver.js.map +0 -1
- package/dist/engine/singleton.d.ts +0 -95
- package/dist/engine/singleton.d.ts.map +0 -1
- package/dist/engine/singleton.js +0 -325
- package/dist/engine/singleton.js.map +0 -1
- package/dist/engine/types.d.ts +0 -402
- package/dist/engine/types.d.ts.map +0 -1
- package/dist/engine/types.js +0 -22
- package/dist/engine/types.js.map +0 -1
- package/dist/engine-binary-resolver.js +0 -110
- package/dist/engine-binary-resolver.test.js +0 -61
- package/dist/engine-cli.js +0 -60
- package/dist/engine-client.js +0 -301
- package/dist/engine-client.test.js +0 -118
- package/dist/functions/chain_state.d.ts +0 -51
- package/dist/functions/chain_state.d.ts.map +0 -1
- package/dist/functions/chain_state.js +0 -59
- package/dist/functions/chain_state.js.map +0 -1
- package/dist/hooks/drift-catalog.d.ts +0 -68
- package/dist/hooks/drift-catalog.d.ts.map +0 -1
- package/dist/hooks/drift-catalog.js +0 -184
- package/dist/hooks/drift-catalog.js.map +0 -1
- package/dist/hooks/drift-catalog.test.js +0 -154
- package/dist/hooks/drift-patterns.d.ts +0 -110
- package/dist/hooks/drift-patterns.d.ts.map +0 -1
- package/dist/hooks/drift-patterns.js +0 -289
- package/dist/hooks/drift-patterns.js.map +0 -1
- package/dist/hooks/drift-patterns.test.js +0 -325
- package/dist/hooks/engine-vocab-gate.d.ts +0 -108
- package/dist/hooks/engine-vocab-gate.d.ts.map +0 -1
- package/dist/hooks/engine-vocab-gate.js +0 -225
- package/dist/hooks/engine-vocab-gate.js.map +0 -1
- package/dist/hooks/engine-vocab-gate.test.js +0 -170
- package/dist/hooks/heartbeat.d.ts +0 -107
- package/dist/hooks/heartbeat.d.ts.map +0 -1
- package/dist/hooks/heartbeat.js +0 -316
- package/dist/hooks/heartbeat.js.map +0 -1
- package/dist/hooks/heartbeat.test.js +0 -393
- package/dist/hooks/honesty-ledger-session-scope.test.js +0 -100
- package/dist/hooks/honesty-ledger.d.ts +0 -123
- package/dist/hooks/honesty-ledger.d.ts.map +0 -1
- package/dist/hooks/honesty-ledger.js +0 -226
- package/dist/hooks/honesty-ledger.js.map +0 -1
- package/dist/hooks/honesty-ledger.test.js +0 -466
- package/dist/hooks/inline-report-check.d.ts +0 -63
- package/dist/hooks/inline-report-check.d.ts.map +0 -1
- package/dist/hooks/inline-report-check.js +0 -88
- package/dist/hooks/inline-report-check.js.map +0 -1
- package/dist/hooks/inline-report-check.test.js +0 -96
- package/dist/hooks/pre-tool-use.d.ts +0 -62
- package/dist/hooks/pre-tool-use.d.ts.map +0 -1
- package/dist/hooks/pre-tool-use.js +0 -342
- package/dist/hooks/pre-tool-use.js.map +0 -1
- package/dist/hooks/pre-tool-use.test.js +0 -134
- package/dist/hooks/session-end.d.ts +0 -15
- package/dist/hooks/session-end.d.ts.map +0 -1
- package/dist/hooks/session-end.js +0 -60
- package/dist/hooks/session-end.js.map +0 -1
- package/dist/hooks/session-end.test.js +0 -52
- package/dist/hooks/stop.d.ts +0 -35
- package/dist/hooks/stop.d.ts.map +0 -1
- package/dist/hooks/stop.js +0 -136
- package/dist/hooks/stop.js.map +0 -1
- package/dist/hooks/transcript-active-task.test.js +0 -342
- package/dist/hooks/transcript.d.ts +0 -26
- package/dist/hooks/transcript.d.ts.map +0 -1
- package/dist/hooks/transcript.js +0 -266
- package/dist/hooks/transcript.js.map +0 -1
- package/dist/hooks/transcript.test.js +0 -103
- package/dist/hooks/user-prompt-submit.d.ts +0 -74
- package/dist/hooks/user-prompt-submit.d.ts.map +0 -1
- package/dist/hooks/user-prompt-submit.js +0 -256
- package/dist/hooks/user-prompt-submit.js.map +0 -1
- package/dist/hooks/user-prompt-submit.test.js +0 -118
- package/dist/hooks/versioning-gate.d.ts +0 -101
- package/dist/hooks/versioning-gate.d.ts.map +0 -1
- package/dist/hooks/versioning-gate.js +0 -245
- package/dist/hooks/versioning-gate.js.map +0 -1
- package/dist/hooks/versioning-gate.test.js +0 -368
- package/dist/hooks/workflow-gate.d.ts +0 -64
- package/dist/hooks/workflow-gate.d.ts.map +0 -1
- package/dist/hooks/workflow-gate.js +0 -152
- package/dist/hooks/workflow-gate.js.map +0 -1
- package/dist/hooks/workflow-gate.test.js +0 -197
- package/dist/hooks-cli.d.ts +0 -25
- package/dist/hooks-cli.d.ts.map +0 -1
- package/dist/hooks-cli.js +0 -286
- package/dist/hooks-cli.js.map +0 -1
- package/dist/hooks-cli.test.js +0 -148
- package/dist/origin.d.ts +0 -16
- package/dist/origin.d.ts.map +0 -1
- package/dist/origin.js +0 -92
- package/dist/origin.js.map +0 -1
- package/dist/packs/seed_lessons_ingest.d.ts +0 -30
- package/dist/packs/seed_lessons_ingest.d.ts.map +0 -1
- package/dist/packs/seed_lessons_ingest.js +0 -107
- package/dist/packs/seed_lessons_ingest.js.map +0 -1
- package/dist/project-cli.d.ts +0 -7
- package/dist/project-cli.d.ts.map +0 -1
- package/dist/project-cli.js +0 -145
- package/dist/project-cli.js.map +0 -1
- package/dist/project.d.ts +0 -127
- package/dist/project.d.ts.map +0 -1
- package/dist/project.js +0 -281
- package/dist/project.js.map +0 -1
- package/dist/project.test.js +0 -287
- package/dist/rag/backends/loop_engine.d.ts +0 -61
- package/dist/rag/backends/loop_engine.d.ts.map +0 -1
- package/dist/rag/backends/loop_engine.js +0 -160
- package/dist/rag/backends/loop_engine.js.map +0 -1
- package/dist/recall.d.ts +0 -82
- package/dist/recall.d.ts.map +0 -1
- package/dist/recall.js +0 -81
- package/dist/recall.js.map +0 -1
- package/dist/runtime/agent_bridge/autospawn.d.ts +0 -131
- package/dist/runtime/agent_bridge/autospawn.d.ts.map +0 -1
- package/dist/runtime/agent_bridge/autospawn.js +0 -251
- package/dist/runtime/agent_bridge/autospawn.js.map +0 -1
- package/dist/runtime/chain_state.d.ts +0 -124
- package/dist/runtime/chain_state.d.ts.map +0 -1
- package/dist/runtime/chain_state.js +0 -189
- package/dist/runtime/chain_state.js.map +0 -1
- package/dist/runtime/hooks/permission_decision.d.ts +0 -34
- package/dist/runtime/hooks/permission_decision.d.ts.map +0 -1
- package/dist/runtime/hooks/permission_decision.js +0 -39
- package/dist/runtime/hooks/permission_decision.js.map +0 -1
- package/dist/runtime/workflow_fsm.d.ts +0 -21
- package/dist/runtime/workflow_fsm.d.ts.map +0 -1
- package/dist/runtime/workflow_fsm.js +0 -25
- package/dist/runtime/workflow_fsm.js.map +0 -1
- package/dist/runtime/workflow_map.d.ts +0 -26
- package/dist/runtime/workflow_map.d.ts.map +0 -1
- package/dist/runtime/workflow_map.js +0 -38
- package/dist/runtime/workflow_map.js.map +0 -1
- package/dist/scope.d.ts +0 -48
- package/dist/scope.d.ts.map +0 -1
- package/dist/scope.js +0 -111
- package/dist/scope.js.map +0 -1
- package/dist/setup/cli/topic_create_step.d.ts +0 -84
- package/dist/setup/cli/topic_create_step.d.ts.map +0 -1
- package/dist/setup/cli/topic_create_step.js +0 -213
- package/dist/setup/cli/topic_create_step.js.map +0 -1
- package/dist/system-export.d.ts +0 -65
- package/dist/system-export.d.ts.map +0 -1
- package/dist/system-export.js +0 -194
- package/dist/system-export.js.map +0 -1
- package/dist/utterance/classifier.d.ts +0 -53
- package/dist/utterance/classifier.d.ts.map +0 -1
- package/dist/utterance/classifier.js +0 -184
- package/dist/utterance/classifier.js.map +0 -1
- package/dist/utterance/classifier.test.js +0 -147
|
@@ -1,313 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Chat-daemon worker entrypoint (v0.7.1 Phase A).
|
|
3
|
-
*
|
|
4
|
-
* Spawned as a detached child by `lifecycle.startDaemon()`. Owns the
|
|
5
|
-
* single long-poll connection per chat platform. The MCP server side
|
|
6
|
-
* stays out of the polling business entirely — outbound RPC (Phase B)
|
|
7
|
-
* and inbox tailing (Phase C) replace the in-process gateway.
|
|
8
|
-
*
|
|
9
|
-
* Lifecycle inside the worker:
|
|
10
|
-
* 1. Write our PID to ~/.opensquid/chat-daemon.pid
|
|
11
|
-
* 2. Build the chat gateway from ~/.opensquid/config.json
|
|
12
|
-
* 3. Start every configured adapter (their long-poll loops run as
|
|
13
|
-
* side effects of start())
|
|
14
|
-
* 4. Install SIGTERM / SIGINT handlers that stop the gateway and
|
|
15
|
-
* remove the pidfile before exit
|
|
16
|
-
* 5. Park on process.stdin (which is /dev/null in detached mode)
|
|
17
|
-
* so the event loop stays alive
|
|
18
|
-
*
|
|
19
|
-
* Crash behavior: any unhandled exception from gateway.start() prints
|
|
20
|
-
* to the (parent-redirected) log file and exits non-zero. The pidfile
|
|
21
|
-
* is cleaned up in the SIGTERM handler — if we crash before installing
|
|
22
|
-
* it, the pidfile may linger, and the next `status` call will report
|
|
23
|
-
* `stale_pid` (lifecycle.startDaemon cleans up stale pidfiles before
|
|
24
|
-
* spawning).
|
|
25
|
-
*/
|
|
26
|
-
import { promises as fs } from "node:fs";
|
|
27
|
-
import { buildChatGateway } from "../factory.js";
|
|
28
|
-
import { appendToInbox } from "./inbox.js";
|
|
29
|
-
import { daemonPaths } from "./lifecycle.js";
|
|
30
|
-
import { buildRoutingIndex, loadAllProjectChatRouting } from "./routing.js";
|
|
31
|
-
import { RpcServer } from "./rpc-server.js";
|
|
32
|
-
let gateway = null;
|
|
33
|
-
let rpcServer = null;
|
|
34
|
-
let routingIndex = new Map();
|
|
35
|
-
let routingPollTimer = null;
|
|
36
|
-
let pidFile = null;
|
|
37
|
-
let shuttingDown = false;
|
|
38
|
-
export async function runDaemonWorker(dataRoot) {
|
|
39
|
-
const paths = daemonPaths(dataRoot);
|
|
40
|
-
pidFile = paths.pidFile;
|
|
41
|
-
// Write pidfile FIRST so a status check after spawn sees the worker
|
|
42
|
-
// promptly. Truncate-write is the right semantic — any previous
|
|
43
|
-
// pidfile is stale by definition (we already verified no live daemon
|
|
44
|
-
// existed in lifecycle.startDaemon).
|
|
45
|
-
await fs.writeFile(pidFile, `${process.pid}\n`, "utf8");
|
|
46
|
-
log(`[chat-daemon] worker booted pid=${process.pid} cwd=${process.cwd()}`);
|
|
47
|
-
// Build + start the gateway. If config is empty, no adapters
|
|
48
|
-
// activate and the daemon parks idle — useful for testing the
|
|
49
|
-
// lifecycle without configuring a real bot token.
|
|
50
|
-
try {
|
|
51
|
-
// 0.7.5 (#148): log which source each platform's token came from
|
|
52
|
-
// (env / env-file / config-json) so operators can debug "which
|
|
53
|
-
// bot is this daemon actually using" without exposing the secret.
|
|
54
|
-
try {
|
|
55
|
-
const { loadChatConfigWithSources } = await import("../config.js");
|
|
56
|
-
const { sources } = await loadChatConfigWithSources(dataRoot);
|
|
57
|
-
const lines = [];
|
|
58
|
-
if (sources.telegram)
|
|
59
|
-
lines.push(`telegram=${sources.telegram}`);
|
|
60
|
-
if (sources.discord)
|
|
61
|
-
lines.push(`discord=${sources.discord}`);
|
|
62
|
-
if (sources.slack_bot)
|
|
63
|
-
lines.push(`slack_bot=${sources.slack_bot}`);
|
|
64
|
-
if (sources.slack_app)
|
|
65
|
-
lines.push(`slack_app=${sources.slack_app}`);
|
|
66
|
-
if (lines.length) {
|
|
67
|
-
log(`[chat-daemon] token sources: ${lines.join(" ")}${sources.env_file_path ? ` (env-file: ${sources.env_file_path})` : ""}`);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
catch (logErr) {
|
|
71
|
-
log(`[chat-daemon] could not log token sources (non-fatal): ${logErr instanceof Error ? logErr.message : logErr}`);
|
|
72
|
-
}
|
|
73
|
-
const built = await buildChatGateway({ dataRoot });
|
|
74
|
-
gateway = built.gateway;
|
|
75
|
-
log(`[chat-daemon] activating platforms: ${built.activated.join(",") || "(none)"}`);
|
|
76
|
-
if (built.issues.length) {
|
|
77
|
-
for (const i of built.issues) {
|
|
78
|
-
log(`[chat-daemon] config issue ${i.platform}.${i.field}: ${i.problem}`);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
// Phase C: load per-project chat-routing.json files and build the
|
|
82
|
-
// chat_id → project_uuid index BEFORE attaching the inbound
|
|
83
|
-
// handler. The handler uses this index to route messages to
|
|
84
|
-
// per-project inboxes.
|
|
85
|
-
routingIndex = await rebuildRoutingIndex(dataRoot);
|
|
86
|
-
log(`[chat-daemon] routing index built: ${routingIndex.size} inbound channels mapped`);
|
|
87
|
-
gateway.onMessage(async (msg) => {
|
|
88
|
-
// v0.5.94 (WAB.2 Part A / TG.1 (a)): DM-first routing.
|
|
89
|
-
//
|
|
90
|
-
// Key precedence:
|
|
91
|
-
// 1. DM key (`telegram:dm:<user_id>`) when the message is a DM —
|
|
92
|
-
// defined as `msg.channel === "telegram:" + msg.senderId`.
|
|
93
|
-
// The Telegram adapter formats DM chats as `telegram:<chat_id>`
|
|
94
|
-
// where chat_id === from.id, so the equality check is the
|
|
95
|
-
// canonical Telegram private-chat indicator. This also rejects
|
|
96
|
-
// group-message-from-self spoofs (group chat.id is negative
|
|
97
|
-
// for supergroups; senderId is positive; they cannot collide).
|
|
98
|
-
// 2. Topic-specific key (`<channel>:<threadId>`) when the message
|
|
99
|
-
// is in a forum topic.
|
|
100
|
-
// 3. Channel-only key (`<channel>`).
|
|
101
|
-
//
|
|
102
|
-
// Strict topic whitelist preserved per TG.1 (d) — a message in a
|
|
103
|
-
// topic NOT listed by any project's `inbound_topic_ids` will not
|
|
104
|
-
// fall back to the chat-only key (because `collectInboundChannels`
|
|
105
|
-
// does not emit the chat-only key when `inbound_topic_ids` is set
|
|
106
|
-
// on that project). Such messages orphan, which is the documented
|
|
107
|
-
// security-correct default.
|
|
108
|
-
const isDm = msg.platform === 'telegram' && msg.channel === `telegram:${msg.senderId}`;
|
|
109
|
-
const dmKey = isDm ? `telegram:dm:${msg.senderId}` : null;
|
|
110
|
-
const topicKey = msg.threadId ? `${msg.channel}:${msg.threadId}` : null;
|
|
111
|
-
const projectUuid = (dmKey ? routingIndex.get(dmKey) : undefined) ??
|
|
112
|
-
(topicKey ? routingIndex.get(topicKey) : undefined) ??
|
|
113
|
-
routingIndex.get(msg.channel) ??
|
|
114
|
-
null;
|
|
115
|
-
try {
|
|
116
|
-
const r = await appendToInbox(msg, projectUuid, dataRoot);
|
|
117
|
-
log(`[chat-daemon] inbox ← ${msg.channel} (${msg.text.slice(0, 60).replace(/\n/g, " ")}…) → ${r.destination}${r.project_uuid ? "/" + r.project_uuid : ""}`);
|
|
118
|
-
}
|
|
119
|
-
catch (err) {
|
|
120
|
-
log(`[chat-daemon] inbox append failed for ${msg.channel}: ${err instanceof Error ? err.message : err}`);
|
|
121
|
-
}
|
|
122
|
-
// TPS.6 patch 2 (v0.5.126) — broadcast to long-lived UDS
|
|
123
|
-
// subscribers. The JSONL file write above is the durable record;
|
|
124
|
-
// this push is the low-latency delivery. We broadcast on a
|
|
125
|
-
// single key — the most-specific one available: topic-suffixed
|
|
126
|
-
// `<channel>:<threadId>` if present, else bare `<channel>`.
|
|
127
|
-
// This mirrors the routing-index key semantics from
|
|
128
|
-
// `collectInboundChannels`: a project that registers only
|
|
129
|
-
// `chat_id:thread_id` doesn't want to receive messages from
|
|
130
|
-
// other topics in the same supergroup. Wildcard subscribers
|
|
131
|
-
// (chat_ids=[]) see every message regardless of key shape.
|
|
132
|
-
// Fire-and-forget — subscriber write failures are handled by
|
|
133
|
-
// the registry's own socket lifecycle hooks.
|
|
134
|
-
if (rpcServer !== null) {
|
|
135
|
-
const broadcastKey = msg.threadId
|
|
136
|
-
? `${msg.channel}:${msg.threadId}`
|
|
137
|
-
: msg.channel;
|
|
138
|
-
const notif = {
|
|
139
|
-
jsonrpc: "2.0",
|
|
140
|
-
method: "inbound_message",
|
|
141
|
-
params: {
|
|
142
|
-
delivery_id: `del-${Date.now().toString()}-${Math.random().toString(36).slice(2, 10)}`,
|
|
143
|
-
message_id: msg.id,
|
|
144
|
-
platform: msg.platform,
|
|
145
|
-
channel: msg.channel,
|
|
146
|
-
...(msg.threadId !== undefined ? { thread_id: msg.threadId } : {}),
|
|
147
|
-
sender: msg.sender,
|
|
148
|
-
sender_id: msg.senderId,
|
|
149
|
-
text: msg.text,
|
|
150
|
-
received_at: msg.receivedAt.toISOString(),
|
|
151
|
-
mentions_bot: msg.mentionsBot,
|
|
152
|
-
},
|
|
153
|
-
};
|
|
154
|
-
rpcServer.subscribers.broadcast(broadcastKey, notif);
|
|
155
|
-
}
|
|
156
|
-
});
|
|
157
|
-
await gateway.start();
|
|
158
|
-
log(`[chat-daemon] gateway start complete`);
|
|
159
|
-
// v0.5.89 (TG.2) — startup reachability check against each
|
|
160
|
-
// unique inbound chat_id. Catches kicked-from-supergroup (403),
|
|
161
|
-
// stale chat_id (400), and network errors immediately at startup.
|
|
162
|
-
// Best-effort: failures don't block daemon startup, just log
|
|
163
|
-
// warnings so operators can see what's wrong without inspecting
|
|
164
|
-
// hours of empty log noise. The check is Telegram-only for now;
|
|
165
|
-
// Discord/Slack equivalents land in a follow-up if needed.
|
|
166
|
-
try {
|
|
167
|
-
const { verifyTelegramChats, formatReachabilityLine } = await import("./health-check.js");
|
|
168
|
-
const { loadChatConfig } = await import("../config.js");
|
|
169
|
-
const chatConfig = await loadChatConfig(dataRoot);
|
|
170
|
-
const tgToken = chatConfig.telegram?.bot_token;
|
|
171
|
-
// Collect unique chat_ids referenced across all projects' Telegram
|
|
172
|
-
// routing (skip dm: and topic-suffixed keys — getChat takes only
|
|
173
|
-
// the chat_id, no topic).
|
|
174
|
-
const chatIds = new Set();
|
|
175
|
-
const configs = await loadAllProjectChatRouting(dataRoot);
|
|
176
|
-
for (const cfg of configs.values()) {
|
|
177
|
-
for (const id of cfg.telegram?.inbound_chat_ids ?? [])
|
|
178
|
-
chatIds.add(id);
|
|
179
|
-
}
|
|
180
|
-
if (chatIds.size > 0 && tgToken) {
|
|
181
|
-
const results = await verifyTelegramChats(tgToken, [...chatIds]);
|
|
182
|
-
for (const r of results)
|
|
183
|
-
log(formatReachabilityLine(r));
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
catch (err) {
|
|
187
|
-
log(`[chat-daemon] chat-reachability check skipped (non-fatal): ${err instanceof Error ? err.message : err}`);
|
|
188
|
-
}
|
|
189
|
-
// RPC server listens for outbound send() calls from per-project
|
|
190
|
-
// MCP servers. Starting it AFTER gateway.start() means clients
|
|
191
|
-
// that connect successfully are guaranteed a fully-warmed gateway.
|
|
192
|
-
rpcServer = new RpcServer({ gateway, dataRoot, version: "v0.7.1-phase-c" });
|
|
193
|
-
await rpcServer.listen();
|
|
194
|
-
// Poll the routing files every 30s so operators can edit a
|
|
195
|
-
// chat-routing.json and have the daemon pick it up without a full
|
|
196
|
-
// restart. Polling is the most portable option (fs.watch behavior
|
|
197
|
-
// varies across macOS/Linux/Windows + recursive support).
|
|
198
|
-
routingPollTimer = setInterval(() => {
|
|
199
|
-
void (async () => {
|
|
200
|
-
try {
|
|
201
|
-
const next = await rebuildRoutingIndex(dataRoot);
|
|
202
|
-
if (!sameIndex(routingIndex, next)) {
|
|
203
|
-
routingIndex = next;
|
|
204
|
-
log(`[chat-daemon] routing reload: ${routingIndex.size} inbound channels mapped`);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
catch (err) {
|
|
208
|
-
log(`[chat-daemon] routing reload failed (non-fatal): ${err instanceof Error ? err.message : err}`);
|
|
209
|
-
}
|
|
210
|
-
})();
|
|
211
|
-
}, 30_000);
|
|
212
|
-
log(`[chat-daemon] rpc server listening; entering park loop`);
|
|
213
|
-
}
|
|
214
|
-
catch (err) {
|
|
215
|
-
log(`[chat-daemon] FATAL: gateway start failed: ${err instanceof Error ? err.stack : err}`);
|
|
216
|
-
await cleanup();
|
|
217
|
-
process.exit(1);
|
|
218
|
-
}
|
|
219
|
-
// Signal handlers. SIGTERM = graceful, SIGINT = also graceful (for
|
|
220
|
-
// manual `kill` during dev). Each calls cleanup() exactly once.
|
|
221
|
-
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
222
|
-
process.on("SIGINT", () => void shutdown("SIGINT"));
|
|
223
|
-
// Park forever. process.stdin.resume() does NOT work here because
|
|
224
|
-
// the parent spawned us with `stdio: ['ignore', ...]` — there's no
|
|
225
|
-
// FD 0 to poll. An unresolved Promise alone won't hold the event
|
|
226
|
-
// loop either; Node exits when nothing's scheduled. The reliable
|
|
227
|
-
// pattern is a long-interval no-op timer (~12 days per tick); the
|
|
228
|
-
// tick is a microsecond of CPU and easily survives clock jitter.
|
|
229
|
-
// Signal handlers are independently registered above and still fire.
|
|
230
|
-
setInterval(() => {
|
|
231
|
-
/* keep-alive heartbeat */
|
|
232
|
-
}, 1 << 30);
|
|
233
|
-
// TypeScript demands a return path even though we never reach here.
|
|
234
|
-
return await new Promise(() => {
|
|
235
|
-
/* never resolves; held alive by the heartbeat interval */
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
async function shutdown(signal) {
|
|
239
|
-
if (shuttingDown)
|
|
240
|
-
return;
|
|
241
|
-
shuttingDown = true;
|
|
242
|
-
log(`[chat-daemon] ${signal} received, shutting down...`);
|
|
243
|
-
if (routingPollTimer)
|
|
244
|
-
clearInterval(routingPollTimer);
|
|
245
|
-
// TPS.6 patch 2 (v0.5.126) — tell subscribers we're going away
|
|
246
|
-
// BEFORE closing the RPC server. The shutdown notification gives
|
|
247
|
-
// MCP bridges a clean signal to back off their reconnect loop
|
|
248
|
-
// instead of treating the disconnect as transient and immediately
|
|
249
|
-
// retrying.
|
|
250
|
-
try {
|
|
251
|
-
if (rpcServer)
|
|
252
|
-
rpcServer.subscribers.shutdown(signal);
|
|
253
|
-
}
|
|
254
|
-
catch (err) {
|
|
255
|
-
log(`[chat-daemon] subscriber shutdown notify error (non-fatal): ${err instanceof Error ? err.message : err}`);
|
|
256
|
-
}
|
|
257
|
-
try {
|
|
258
|
-
if (rpcServer)
|
|
259
|
-
await rpcServer.close();
|
|
260
|
-
}
|
|
261
|
-
catch (err) {
|
|
262
|
-
log(`[chat-daemon] rpc close error (non-fatal): ${err instanceof Error ? err.message : err}`);
|
|
263
|
-
}
|
|
264
|
-
try {
|
|
265
|
-
if (gateway)
|
|
266
|
-
await gateway.shutdown();
|
|
267
|
-
}
|
|
268
|
-
catch (err) {
|
|
269
|
-
log(`[chat-daemon] gateway.shutdown error (non-fatal): ${err instanceof Error ? err.message : err}`);
|
|
270
|
-
}
|
|
271
|
-
await cleanup();
|
|
272
|
-
log(`[chat-daemon] clean exit`);
|
|
273
|
-
process.exit(0);
|
|
274
|
-
}
|
|
275
|
-
async function cleanup() {
|
|
276
|
-
if (pidFile) {
|
|
277
|
-
try {
|
|
278
|
-
await fs.unlink(pidFile);
|
|
279
|
-
}
|
|
280
|
-
catch {
|
|
281
|
-
/* race-tolerant */
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
function log(line) {
|
|
286
|
-
// stdio is already redirected to the log file by the parent's spawn
|
|
287
|
-
// options; plain console.log lands in the right place.
|
|
288
|
-
process.stdout.write(`${new Date().toISOString()} ${line}\n`);
|
|
289
|
-
}
|
|
290
|
-
async function rebuildRoutingIndex(dataRoot) {
|
|
291
|
-
const cfgs = await loadAllProjectChatRouting(dataRoot);
|
|
292
|
-
const { recordCollision } = await import("./collisions.js");
|
|
293
|
-
return buildRoutingIndex(cfgs, (info) => {
|
|
294
|
-
log(`[chat-daemon] routing collision: ${info.channel_key} existing=${info.existing_uuid} newcomer=${info.newcomer_uuid} (latter wins)`);
|
|
295
|
-
// Fire-and-forget: persist + notify happen async; the routing
|
|
296
|
-
// rebuild must not wait. recordCollision swallows its own errors
|
|
297
|
-
// (stderr-logged) so this `void` is intentional.
|
|
298
|
-
void recordCollision({
|
|
299
|
-
info,
|
|
300
|
-
dataRoot,
|
|
301
|
-
...(gateway !== null ? { gateway } : {}),
|
|
302
|
-
});
|
|
303
|
-
});
|
|
304
|
-
}
|
|
305
|
-
function sameIndex(a, b) {
|
|
306
|
-
if (a.size !== b.size)
|
|
307
|
-
return false;
|
|
308
|
-
for (const [k, v] of a) {
|
|
309
|
-
if (b.get(k) !== v)
|
|
310
|
-
return false;
|
|
311
|
-
}
|
|
312
|
-
return true;
|
|
313
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"worker.js","sourceRoot":"","sources":["../../../src.legacy/chat/daemon/worker.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,324 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* workspace-topic.ts — workspace → forum-topic binding primitive
|
|
3
|
-
* (TPS.3 / v0.5.120+).
|
|
4
|
-
*
|
|
5
|
-
* Solves: given a workspace (cwd-resolved uuid + path) and a target
|
|
6
|
-
* supergroup chat_id, ensure the workspace has a bound forum topic +
|
|
7
|
-
* persist the binding to its `chat-routing.json`. Idempotent.
|
|
8
|
-
*
|
|
9
|
-
* Used by:
|
|
10
|
-
* - TPS.4 — `opensquid setup chat` wizard step (mode: "wizard")
|
|
11
|
-
* - TPS.6 — daemon auto-boot on MCP subscribe (mode: "auto-boot")
|
|
12
|
-
*
|
|
13
|
-
* Concurrency: protected by `proper-lockfile` on the per-project
|
|
14
|
-
* `chat-routing.json` so two concurrent invocations for the same
|
|
15
|
-
* workspace can't race-create two topics. The lock window covers
|
|
16
|
-
* (load existing config) → (call createTopic if missing) → (write
|
|
17
|
-
* updated config). Lock retries are bounded; if a stale lock from a
|
|
18
|
-
* crashed prior run blocks acquisition, an `LOCKED` error is thrown
|
|
19
|
-
* upward — callers (wizard, auto-boot) surface this to the user
|
|
20
|
-
* rather than papering over it.
|
|
21
|
-
*
|
|
22
|
-
* Error propagation: this module does NOT swallow errors. RPC failures
|
|
23
|
-
* (bot not admin, network), parse errors, and lock failures all
|
|
24
|
-
* propagate. Callers decide how to surface them (TPS.4 prints to the
|
|
25
|
-
* wizard, TPS.6 logs + falls back to general topic).
|
|
26
|
-
*
|
|
27
|
-
* Rebuild path: same as adapters/telegram.ts — see that file's header
|
|
28
|
-
* for the ad-hoc tsc invocation. `pnpm build` does NOT recompile this
|
|
29
|
-
* file; the chat-daemon worker loads dist/chat/daemon/workspace-topic.js
|
|
30
|
-
* at runtime.
|
|
31
|
-
*/
|
|
32
|
-
import { promises as fs } from "node:fs";
|
|
33
|
-
import { createRequire } from "node:module";
|
|
34
|
-
import * as path from "node:path";
|
|
35
|
-
import * as lockfile from "proper-lockfile";
|
|
36
|
-
// Need synchronous require() to construct the rpc-client lazily without
|
|
37
|
-
// making resolveOrCreateTopic's signature async-on-import. ESM Node 20+
|
|
38
|
-
// exposes createRequire for exactly this case.
|
|
39
|
-
const requireCJS = createRequire(import.meta.url);
|
|
40
|
-
import { loadAllProjectChatRouting, loadProjectChatRouting, projectChatRoutingPath, } from "./routing.js";
|
|
41
|
-
// ---------------------------------------------------------------------
|
|
42
|
-
// Main entrypoint
|
|
43
|
-
// ---------------------------------------------------------------------
|
|
44
|
-
export async function resolveOrCreateTopic(args) {
|
|
45
|
-
const routingPath = projectChatRoutingPath(args.workspaceUuid, args.dataRoot);
|
|
46
|
-
// Lockfile lives next to the routing file; proper-lockfile handles
|
|
47
|
-
// both lock acquisition and the necessary parent-dir creation logic
|
|
48
|
-
// as long as the target exists. Ensure the dir exists first.
|
|
49
|
-
await fs.mkdir(path.dirname(routingPath), { recursive: true });
|
|
50
|
-
// proper-lockfile requires the target file to exist; touch it
|
|
51
|
-
// (empty config) if it doesn't, so the lock can be acquired
|
|
52
|
-
// regardless of whether the workspace has ever had routing set up.
|
|
53
|
-
await ensureRoutingFileExists(routingPath);
|
|
54
|
-
// Retry tuning rationale (TPS.3 pre-research): typical createTopic
|
|
55
|
-
// round-trip is 200-600ms (UDS + HTTPS to api.telegram.org). 8
|
|
56
|
-
// retries with 1.5× backoff at 50ms-800ms gives ~2.4s headroom —
|
|
57
|
-
// plenty for one contender to finish while another waits. Stale is
|
|
58
|
-
// proper-lockfile's default (10s); not setting it explicitly.
|
|
59
|
-
const release = await lockfile.lock(routingPath, {
|
|
60
|
-
retries: { retries: 8, factor: 1.5, minTimeout: 50, maxTimeout: 800 },
|
|
61
|
-
});
|
|
62
|
-
try {
|
|
63
|
-
const existing = await loadProjectChatRouting(args.workspaceUuid, args.dataRoot);
|
|
64
|
-
assertAutoBoundInvariant(existing, routingPath);
|
|
65
|
-
const bound = existing?.telegram?.auto_bound;
|
|
66
|
-
if (bound && Number.isFinite(bound.topic_id) && bound.topic_id > 0) {
|
|
67
|
-
// Idempotent — already bound, return existing.
|
|
68
|
-
// Sanity check: if the auto_bound.workspace_uuid disagrees with
|
|
69
|
-
// the outer uuid (the directory name), log on stderr but trust
|
|
70
|
-
// the outer uuid as authoritative.
|
|
71
|
-
if (bound.workspace_uuid !== args.workspaceUuid) {
|
|
72
|
-
process.stderr.write(`[workspace-topic] auto_bound.workspace_uuid (${bound.workspace_uuid}) ≠ outer uuid (${args.workspaceUuid}) for ${routingPath}; using existing binding\n`);
|
|
73
|
-
}
|
|
74
|
-
return { topicId: bound.topic_id, topicName: bound.topic_name, created: false };
|
|
75
|
-
}
|
|
76
|
-
const name = deriveTopicName(args.workspacePath, args.workspaceUuid);
|
|
77
|
-
const client = args.rpcClient ?? defaultRpcClient(args.dataRoot);
|
|
78
|
-
const created = await client.createTopic({
|
|
79
|
-
platform: "telegram",
|
|
80
|
-
chat_id: args.chatId,
|
|
81
|
-
name,
|
|
82
|
-
});
|
|
83
|
-
const nextAutoBound = {
|
|
84
|
-
workspace_path: args.workspacePath,
|
|
85
|
-
workspace_uuid: args.workspaceUuid,
|
|
86
|
-
topic_id: created.message_thread_id,
|
|
87
|
-
topic_name: created.name,
|
|
88
|
-
created_at: new Date().toISOString(),
|
|
89
|
-
created_by: args.mode,
|
|
90
|
-
};
|
|
91
|
-
const merged = {
|
|
92
|
-
...(existing ?? {}),
|
|
93
|
-
telegram: {
|
|
94
|
-
...(existing?.telegram ?? {}),
|
|
95
|
-
// Persist both auto_bound metadata + the actual routing field
|
|
96
|
-
// (inbound_topic_ids) so the routing index picks up the new
|
|
97
|
-
// binding on its next ~30s hot-reload without separate writes.
|
|
98
|
-
inbound_topic_ids: mergeTopicIds(existing?.telegram?.inbound_topic_ids, created.message_thread_id),
|
|
99
|
-
inbound_chat_ids: mergeChatIds(existing?.telegram?.inbound_chat_ids, args.chatId),
|
|
100
|
-
auto_bound: nextAutoBound,
|
|
101
|
-
},
|
|
102
|
-
};
|
|
103
|
-
try {
|
|
104
|
-
await persistRoutingAtomic(routingPath, merged);
|
|
105
|
-
}
|
|
106
|
-
catch (persistErr) {
|
|
107
|
-
// TPS.3 pre-research, choice #6 partial-failure compensation:
|
|
108
|
-
// createTopic SUCCEEDED but persist FAILED — Telegram has a real
|
|
109
|
-
// topic we cannot reference. Log it to a recovery file so the
|
|
110
|
-
// user can clean up (delete the orphan topic manually) instead
|
|
111
|
-
// of accumulating ghost topics on every retry. Don't try to
|
|
112
|
-
// rollback (delete the topic) here — that requires a second
|
|
113
|
-
// RPC call that could also fail, compounding the problem.
|
|
114
|
-
// The user-facing surface lives in TPS.5 collision channel.
|
|
115
|
-
await recordOrphanTopic(args.dataRoot, {
|
|
116
|
-
chat_id: args.chatId,
|
|
117
|
-
topic_id: created.message_thread_id,
|
|
118
|
-
topic_name: created.name,
|
|
119
|
-
workspace_uuid: args.workspaceUuid,
|
|
120
|
-
workspace_path: args.workspacePath,
|
|
121
|
-
mode: args.mode,
|
|
122
|
-
persist_error: persistErr instanceof Error ? persistErr.message : String(persistErr),
|
|
123
|
-
occurred_at: new Date().toISOString(),
|
|
124
|
-
});
|
|
125
|
-
throw persistErr;
|
|
126
|
-
}
|
|
127
|
-
return {
|
|
128
|
-
topicId: created.message_thread_id,
|
|
129
|
-
topicName: created.name,
|
|
130
|
-
created: true,
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
finally {
|
|
134
|
-
await release();
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
// ---------------------------------------------------------------------
|
|
138
|
-
// Helpers (exported for unit tests)
|
|
139
|
-
// ---------------------------------------------------------------------
|
|
140
|
-
/**
|
|
141
|
-
* Derive a deterministic, human-readable topic name from the workspace
|
|
142
|
-
* path + uuid. The basename of the path is the most user-recognisable
|
|
143
|
-
* part; the uuid prefix disambiguates two workspaces with the same
|
|
144
|
-
* basename. Examples:
|
|
145
|
-
*
|
|
146
|
-
* deriveTopicName("/Users/slee/projects/loop", "da96385b-...") =
|
|
147
|
-
* "loop · da96385b"
|
|
148
|
-
* deriveTopicName("/", "abc12345-...") = "root · abc12345"
|
|
149
|
-
*/
|
|
150
|
-
export function deriveTopicName(workspacePath, workspaceUuid) {
|
|
151
|
-
const basenameRaw = path.basename(workspacePath) || "root";
|
|
152
|
-
// Telegram limit per [aiogram docs](https://docs.aiogram.dev/en/latest/api/methods/create_forum_topic.html)
|
|
153
|
-
// is 1-128 chars. Cap basename at 48 to leave headroom for the
|
|
154
|
-
// " · 12345678" suffix (11 chars) — total max output ~59 chars.
|
|
155
|
-
// 48 is conservative: Telegram client truncates topic-list display
|
|
156
|
-
// at ~30-35 chars anyway. Pre-research verdict #4.
|
|
157
|
-
const basename = basenameRaw.length > 48 ? `${basenameRaw.slice(0, 45)}...` : basenameRaw;
|
|
158
|
-
const uuidShort = workspaceUuid.slice(0, 8);
|
|
159
|
-
return `${basename} · ${uuidShort}`;
|
|
160
|
-
}
|
|
161
|
-
/**
|
|
162
|
-
* Merge a single new topic_id into an optional existing array. Avoids
|
|
163
|
-
* duplicates while preserving order (existing first, new last).
|
|
164
|
-
*/
|
|
165
|
-
export function mergeTopicIds(existing, newId) {
|
|
166
|
-
if (!existing || existing.length === 0)
|
|
167
|
-
return [newId];
|
|
168
|
-
if (existing.includes(newId))
|
|
169
|
-
return existing;
|
|
170
|
-
return [...existing, newId];
|
|
171
|
-
}
|
|
172
|
-
/**
|
|
173
|
-
* Same as mergeTopicIds for chat_ids (strings).
|
|
174
|
-
*/
|
|
175
|
-
export function mergeChatIds(existing, newId) {
|
|
176
|
-
if (!existing || existing.length === 0)
|
|
177
|
-
return [newId];
|
|
178
|
-
if (existing.includes(newId))
|
|
179
|
-
return existing;
|
|
180
|
-
return [...existing, newId];
|
|
181
|
-
}
|
|
182
|
-
/**
|
|
183
|
-
* Clear an existing auto_bound block (TPS.7 stale-topic lifecycle).
|
|
184
|
-
* Leaves `inbound_topic_ids` alone — caller decides whether to also
|
|
185
|
-
* scrub those (typically yes, since the stale topic_id no longer
|
|
186
|
-
* exists). Returns true if a binding was cleared, false if none.
|
|
187
|
-
*/
|
|
188
|
-
export async function clearBinding(args) {
|
|
189
|
-
const routingPath = projectChatRoutingPath(args.workspaceUuid, args.dataRoot);
|
|
190
|
-
await fs.mkdir(path.dirname(routingPath), { recursive: true });
|
|
191
|
-
await ensureRoutingFileExists(routingPath);
|
|
192
|
-
const release = await lockfile.lock(routingPath, {
|
|
193
|
-
retries: { retries: 8, factor: 1.5, minTimeout: 50, maxTimeout: 800 },
|
|
194
|
-
stale: 10_000,
|
|
195
|
-
});
|
|
196
|
-
try {
|
|
197
|
-
const existing = await loadProjectChatRouting(args.workspaceUuid, args.dataRoot);
|
|
198
|
-
if (!existing?.telegram?.auto_bound)
|
|
199
|
-
return false;
|
|
200
|
-
const staleTopicId = existing.telegram.auto_bound.topic_id;
|
|
201
|
-
const next = {
|
|
202
|
-
...existing,
|
|
203
|
-
telegram: {
|
|
204
|
-
...existing.telegram,
|
|
205
|
-
inbound_topic_ids: (existing.telegram.inbound_topic_ids ?? []).filter((t) => t !== staleTopicId),
|
|
206
|
-
auto_bound: undefined,
|
|
207
|
-
},
|
|
208
|
-
};
|
|
209
|
-
// Drop the auto_bound key entirely (don't leave `auto_bound: undefined`
|
|
210
|
-
// in the JSON output).
|
|
211
|
-
if (next.telegram)
|
|
212
|
-
delete next.telegram.auto_bound;
|
|
213
|
-
if (next.telegram?.inbound_topic_ids?.length === 0)
|
|
214
|
-
delete next.telegram.inbound_topic_ids;
|
|
215
|
-
await persistRoutingAtomic(routingPath, next);
|
|
216
|
-
return true;
|
|
217
|
-
}
|
|
218
|
-
finally {
|
|
219
|
-
await release();
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
/**
|
|
223
|
-
* Find the workspace whose `auto_bound` block claims the given
|
|
224
|
-
* (chat_id, topic_id) pair. Used by TPS.7 stale-topic recovery to
|
|
225
|
-
* locate which workspace owned a now-stale binding so we can
|
|
226
|
-
* `clearBinding` for the right uuid.
|
|
227
|
-
*
|
|
228
|
-
* Returns the workspace uuid on match, or null when:
|
|
229
|
-
* - no project has an auto_bound block at all
|
|
230
|
-
* - no auto_bound block matches BOTH chat_id and topic_id
|
|
231
|
-
* - the matching project's routing config was already cleared by a
|
|
232
|
-
* concurrent recovery (race-safe)
|
|
233
|
-
*
|
|
234
|
-
* Match rule: `auto_bound.topic_id === topicId` AND
|
|
235
|
-
* `inbound_chat_ids.includes(chatId)`.
|
|
236
|
-
* The chat_id check guards against the (rare) case of identical topic_id
|
|
237
|
-
* numbers across two different supergroups; without it we could clear
|
|
238
|
-
* the wrong workspace's binding.
|
|
239
|
-
*
|
|
240
|
-
* Lock-free read: this just scans on-disk routing configs. The actual
|
|
241
|
-
* mutation (`clearBinding`) is lockfile-protected on the per-project
|
|
242
|
-
* routing file, so two concurrent recoveries racing to clear the same
|
|
243
|
-
* binding serialize cleanly — first wins, second sees no binding to
|
|
244
|
-
* clear and returns false.
|
|
245
|
-
*/
|
|
246
|
-
export async function findOwnerOfBinding(args) {
|
|
247
|
-
const all = await loadAllProjectChatRouting(args.dataRoot);
|
|
248
|
-
for (const [uuid, cfg] of all) {
|
|
249
|
-
const bound = cfg.telegram?.auto_bound;
|
|
250
|
-
if (!bound)
|
|
251
|
-
continue;
|
|
252
|
-
if (bound.topic_id !== args.topicId)
|
|
253
|
-
continue;
|
|
254
|
-
const inboundChats = cfg.telegram?.inbound_chat_ids ?? [];
|
|
255
|
-
if (!inboundChats.includes(args.chatId))
|
|
256
|
-
continue;
|
|
257
|
-
return uuid;
|
|
258
|
-
}
|
|
259
|
-
return null;
|
|
260
|
-
}
|
|
261
|
-
// ---------------------------------------------------------------------
|
|
262
|
-
// Internal
|
|
263
|
-
// ---------------------------------------------------------------------
|
|
264
|
-
async function ensureRoutingFileExists(routingPath) {
|
|
265
|
-
try {
|
|
266
|
-
await fs.access(routingPath);
|
|
267
|
-
}
|
|
268
|
-
catch {
|
|
269
|
-
await fs.writeFile(routingPath, "{}\n", { flag: "wx" }).catch((err) => {
|
|
270
|
-
// EEXIST is fine — another process touched it in the race window.
|
|
271
|
-
if (err.code !== "EEXIST")
|
|
272
|
-
throw err;
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
async function persistRoutingAtomic(routingPath, cfg) {
|
|
277
|
-
// Write to a sibling tmp + rename for atomicity (rename(2) is atomic
|
|
278
|
-
// on the same filesystem). Avoids partial-write reads from the daemon's
|
|
279
|
-
// 30s reload loop.
|
|
280
|
-
const tmp = `${routingPath}.${process.pid}.tmp`;
|
|
281
|
-
await fs.writeFile(tmp, JSON.stringify(cfg, null, 2) + "\n", "utf8");
|
|
282
|
-
await fs.rename(tmp, routingPath);
|
|
283
|
-
}
|
|
284
|
-
function defaultRpcClient(dataRoot) {
|
|
285
|
-
// No cache: DaemonClient construction is cheap (just stores config)
|
|
286
|
-
// and caching it across calls broke tests that switch OPENSQUID_HOME
|
|
287
|
-
// per test. Construct fresh; pay the ~no-op cost. Pre-research
|
|
288
|
-
// verdict #7.
|
|
289
|
-
const { DaemonClient } = requireCJS("./rpc-client.js");
|
|
290
|
-
return new DaemonClient(dataRoot ? { dataRoot } : {});
|
|
291
|
-
}
|
|
292
|
-
/**
|
|
293
|
-
* Invariant check: if `auto_bound.topic_id` is set, it MUST appear in
|
|
294
|
-
* `inbound_topic_ids`. Pre-research verdict #9: log a warning on
|
|
295
|
-
* mismatch but do NOT auto-repair (preserves user-edited intent).
|
|
296
|
-
*/
|
|
297
|
-
function assertAutoBoundInvariant(cfg, routingPath) {
|
|
298
|
-
const bound = cfg?.telegram?.auto_bound;
|
|
299
|
-
if (!bound)
|
|
300
|
-
return;
|
|
301
|
-
const inboundTopics = cfg?.telegram?.inbound_topic_ids ?? [];
|
|
302
|
-
if (!inboundTopics.includes(bound.topic_id)) {
|
|
303
|
-
process.stderr.write(`[workspace-topic] invariant warning: auto_bound.topic_id=${bound.topic_id} not in inbound_topic_ids=${JSON.stringify(inboundTopics)} for ${routingPath}; not auto-repairing\n`);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
async function recordOrphanTopic(dataRoot, record) {
|
|
307
|
-
// Pre-research verdict #6: log orphans to a recovery file so the
|
|
308
|
-
// user can clean up (delete the topic manually via Telegram client
|
|
309
|
-
// or via a future TPS.7 cleanup tool) instead of accumulating
|
|
310
|
-
// ghost topics on every retry. Doesn't try to delete the topic
|
|
311
|
-
// (that's a separate RPC call that could ALSO fail, compounding).
|
|
312
|
-
const root = dataRoot ?? process.env.OPENSQUID_HOME;
|
|
313
|
-
if (!root)
|
|
314
|
-
return; // best-effort: nowhere to write
|
|
315
|
-
const recoveryPath = path.join(root, "orphan-topics.jsonl");
|
|
316
|
-
try {
|
|
317
|
-
await fs.mkdir(path.dirname(recoveryPath), { recursive: true });
|
|
318
|
-
await fs.appendFile(recoveryPath, JSON.stringify(record) + "\n", "utf8");
|
|
319
|
-
}
|
|
320
|
-
catch {
|
|
321
|
-
// If even the recovery write fails, give up silently. The original
|
|
322
|
-
// persist error will propagate; that's the load-bearing surface.
|
|
323
|
-
}
|
|
324
|
-
}
|