work-ally 0.2.0-alpha.1
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/AGENTS.md +110 -0
- package/DASHBOARD.md +160 -0
- package/PRODUCT.md +113 -0
- package/README.md +403 -0
- package/ally.sh +171 -0
- package/bridge/src/approval-rules.ts +360 -0
- package/bridge/src/channel-delivery.ts +207 -0
- package/bridge/src/channel-types.ts +22 -0
- package/bridge/src/channels/fake/adapter.ts +31 -0
- package/bridge/src/channels/feishu/adapter.ts +411 -0
- package/bridge/src/channels/feishu/approvals.ts +6 -0
- package/bridge/src/channels/feishu/formatter.ts +276 -0
- package/bridge/src/channels/feishu/normalize.ts +368 -0
- package/bridge/src/codex-config.ts +52 -0
- package/bridge/src/config.ts +240 -0
- package/bridge/src/fake-runtime-client.ts +505 -0
- package/bridge/src/handoff-service.ts +494 -0
- package/bridge/src/logger.ts +194 -0
- package/bridge/src/memory-digest.ts +186 -0
- package/bridge/src/receiver-approval-autonomy.ts +158 -0
- package/bridge/src/receiver-control-core.ts +140 -0
- package/bridge/src/receiver-control-work-session.ts +218 -0
- package/bridge/src/receiver-control.ts +83 -0
- package/bridge/src/receiver-delivery.ts +136 -0
- package/bridge/src/receiver-helpers.ts +96 -0
- package/bridge/src/receiver-human-gate.ts +333 -0
- package/bridge/src/receiver-inbound-preflight.ts +162 -0
- package/bridge/src/receiver-recovery.ts +236 -0
- package/bridge/src/receiver-runtime-callbacks.ts +367 -0
- package/bridge/src/receiver-runtime-policy.ts +132 -0
- package/bridge/src/receiver-runtime-state.ts +124 -0
- package/bridge/src/receiver-support-actions.ts +189 -0
- package/bridge/src/receiver-thread-start.ts +57 -0
- package/bridge/src/receiver-turn-coordination.ts +94 -0
- package/bridge/src/receiver-turn-execution.ts +257 -0
- package/bridge/src/receiver-turn-failure.ts +143 -0
- package/bridge/src/receiver-turn-result.ts +185 -0
- package/bridge/src/receiver-turn-steer.ts +70 -0
- package/bridge/src/receiver-work-session.ts +76 -0
- package/bridge/src/receiver.ts +329 -0
- package/bridge/src/router.ts +62 -0
- package/bridge/src/runtime-client-agent-messages.ts +150 -0
- package/bridge/src/runtime-client-message-dispatch.ts +176 -0
- package/bridge/src/runtime-client-protocol.ts +411 -0
- package/bridge/src/runtime-client-request-ops.ts +56 -0
- package/bridge/src/runtime-client-run-turn.ts +158 -0
- package/bridge/src/runtime-client-thread-ops.ts +270 -0
- package/bridge/src/runtime-client-transport.ts +309 -0
- package/bridge/src/runtime-client-turn-poll.ts +224 -0
- package/bridge/src/runtime-client-turn-read.ts +185 -0
- package/bridge/src/runtime-client-turn-state.ts +105 -0
- package/bridge/src/runtime-client.ts +344 -0
- package/bridge/src/runtime-user-input.ts +403 -0
- package/bridge/src/scheduler.ts +239 -0
- package/bridge/src/server-handoff-command.ts +364 -0
- package/bridge/src/server-main.ts +80 -0
- package/bridge/src/server-routine-command.ts +60 -0
- package/bridge/src/server-routine-execution.ts +222 -0
- package/bridge/src/server-runtime-app-support.ts +107 -0
- package/bridge/src/server-runtime-app.ts +238 -0
- package/bridge/src/server-thread-sync-command.ts +63 -0
- package/bridge/src/server.ts +17 -0
- package/bridge/src/session-store-delivery.ts +220 -0
- package/bridge/src/session-store-human-gate.ts +380 -0
- package/bridge/src/session-store-inbound-acceptance.ts +66 -0
- package/bridge/src/session-store-meta.ts +134 -0
- package/bridge/src/session-store-turn-ledger.ts +272 -0
- package/bridge/src/session-store.ts +380 -0
- package/bridge/src/system-notify.ts +220 -0
- package/bridge/src/thread-sync.ts +200 -0
- package/bridge/src/translator.ts +494 -0
- package/bridge/src/types.ts +289 -0
- package/bridge/src/utils.ts +104 -0
- package/bridge/src/work-session-store.ts +471 -0
- package/docs/.gitkeep +0 -0
- package/docs/architecture/codex-feishu-bridge-proposal.md +2742 -0
- package/docs/completed/FEATURE-feishu-markdown-and-reply-support.md +327 -0
- package/docs/completed/README.md +21 -0
- package/docs/completed/SPEC-approval-autonomy-and-safe-defaults.md +205 -0
- package/docs/completed/SPEC-approval-batch-and-strict-reply-shortcuts.md +153 -0
- package/docs/completed/SPEC-conversation-noise-reduction-and-busy-input-gate.md +538 -0
- package/docs/completed/SPEC-engineering-sop-skillization.md +190 -0
- package/docs/completed/SPEC-faithful-bridge-core-thinning-v2.md +376 -0
- package/docs/completed/SPEC-faithful-bridge-core-thinning.md +1071 -0
- package/docs/completed/SPEC-group-chat-sender-identity.md +301 -0
- package/docs/completed/SPEC-middleware-exception-visibility.md +227 -0
- package/docs/completed/SPEC-nightly-memory-digest-visibility.md +121 -0
- package/docs/completed/SPEC-project-group-chat-human-centered-conversation-mapping.md +326 -0
- package/docs/completed/SPEC-remove-cli-persona-bootstrap.md +201 -0
- package/docs/developer-workflow.md +49 -0
- package/docs/implementation/SPEC-codex-same-machine-session-handoff-implementation.md +239 -0
- package/docs/implementation/test-coverage-map.md +363 -0
- package/docs/implementation/work-ally-implementation-guide.md +790 -0
- package/docs/issues/README.md +10 -0
- package/docs/issues/pending/ANALYSIS-ally-premature-recovery-notice-and-task-state-semantics-2026-03-18.md +295 -0
- package/docs/issues/resolved/ANALYSIS-approval-waiting-visible-but-approval-artifact-missing-2026-03-16.md +466 -0
- package/docs/issues/resolved/ANALYSIS-blocking-state-visible-without-user-actionable-artifact-2026-03-16.md +261 -0
- package/docs/issues/resolved/ANALYSIS-codex-app-server-transport-disconnect-semantics-2026-03-14.md +606 -0
- package/docs/issues/resolved/ANALYSIS-premature-terminalization-on-fresh-thread-poll-and-object-error-leak-2026-03-16.md +348 -0
- package/docs/issues/resolved/ANALYSIS-runtime-turn-delivery-and-recovery-2026-03-14.md +603 -0
- package/docs/issues/resolved/ANALYSIS-self-test-gap-approval-waiting-visible-but-approval-artifact-missing-2026-03-16.md +166 -0
- package/docs/issues/resolved/ANALYSIS-self-test-gap-blocking-state-visible-without-user-actionable-artifact-2026-03-16.md +186 -0
- package/docs/issues/resolved/ANALYSIS-self-test-gap-premature-terminalization-on-fresh-thread-poll-and-object-error-leak-2026-03-16.md +166 -0
- package/docs/issues/resolved/REPORT-ally-runtime-turn-delivery-3b42fb8-2026-03-15.md +373 -0
- package/docs/manual-acceptance.md +127 -0
- package/docs/ops-runbook.md +44 -0
- package/docs/planning/FEATURE-memory-system.md +748 -0
- package/docs/planning/SPEC-active-turn-steer-and-context-compaction-visibility.md +269 -0
- package/docs/planning/SPEC-approval-rules-inheritance-and-local-validation-lane.md +450 -0
- package/docs/planning/SPEC-assistant-persona-bootstrap.md +199 -0
- package/docs/planning/SPEC-assistant-rename.md +610 -0
- package/docs/planning/SPEC-bridge-app-server-protocol-alignment.md +667 -0
- package/docs/planning/SPEC-claude-runtime-host-for-work-ally.md +434 -0
- package/docs/planning/SPEC-cli-feishu-codex-session-unification.md +236 -0
- package/docs/planning/SPEC-codex-same-machine-session-handoff.md +873 -0
- package/docs/planning/SPEC-feishu-reaction-shortcuts.md +282 -0
- package/docs/planning/SPEC-local-stable-release-boundary.md +166 -0
- package/docs/planning/SPEC-managed-thread-entry-and-surface-mobility.md +862 -0
- package/docs/planning/SPEC-minimal-bridge-semantics-and-user-visible-surface.md +362 -0
- package/docs/planning/SPEC-npm-alpha-distribution-and-install-first-release.md +222 -0
- package/docs/planning/SPEC-remove-websocket-runtime-transport.md +364 -0
- package/docs/planning/SPEC-runtime-abstraction-phase-1.md +424 -0
- package/docs/planning/SPEC-runtime-connection-and-turn-recovery-semantics.md +274 -0
- package/docs/planning/SPEC-session-presence-and-state-visibility.md +397 -0
- package/docs/planning/SPEC-skill-first-capability-packaging.md +338 -0
- package/docs/planning/SPEC-stable-archive-contract.md +456 -0
- package/docs/planning/SPEC-supervised-start-boundary.md +127 -0
- package/docs/planning/SPEC-user-barrier-reduction-and-activation.md +832 -0
- package/docs/planning/ally-next.md +1278 -0
- package/docs/planning/assistant-workbench-spec.md +725 -0
- package/docs/planning/product-workbench.md +283 -0
- package/docs/product-onboarding.md +227 -0
- package/docs/product-spec-standard.md +528 -0
- package/docs/troubleshooting.md +45 -0
- package/docs/user-quickstart.md +46 -0
- package/internal/dispatch.sh +95 -0
- package/internal/lib/common.sh +1450 -0
- package/internal/modules/assistant/manage.sh +1312 -0
- package/internal/modules/bootstrap/setup.sh +144 -0
- package/internal/modules/config/init-env.sh +10 -0
- package/internal/modules/global/manage.sh +154 -0
- package/internal/modules/handoff/manage.sh +54 -0
- package/internal/modules/mcp/manage.sh +83 -0
- package/internal/modules/ops/logs.sh +76 -0
- package/internal/modules/routines/manage.sh +55 -0
- package/internal/modules/runtime/assistant-autosave.sh +26 -0
- package/internal/modules/runtime/restart.sh +6 -0
- package/internal/modules/runtime/start.sh +283 -0
- package/internal/modules/runtime/status.sh +194 -0
- package/internal/modules/runtime/stop.sh +55 -0
- package/internal/modules/runtime/supervisor.sh +216 -0
- package/internal/modules/runtime/update.sh +26 -0
- package/package.json +41 -0
- package/runtime/config/.gitkeep +0 -0
- package/runtime/host/.gitkeep +0 -0
- package/runtime/host/healthcheck-codex-app-server.ts +22 -0
- package/runtime/host/ping-pong-codex-app-server.ts +66 -0
- package/runtime/host/probe-codex-app-server.ts +115 -0
- package/skills/archive-reader/SKILL.md +9 -0
- package/skills/feishu-production-debug/SKILL.md +37 -0
- package/skills/feishu-production-debug/references/feishu-debug-order.md +49 -0
- package/skills/feishu-production-debug/references/platform-permission-baseline.md +23 -0
- package/skills/issue-to-spec-triage/SKILL.md +44 -0
- package/skills/issue-to-spec-triage/references/triage-rules.md +66 -0
- package/skills/memory-digest/SKILL.md +9 -0
- package/skills/post-implementation-closure/SKILL.md +39 -0
- package/skills/post-implementation-closure/references/closure-checklist.md +45 -0
- package/skills/post-implementation-closure/references/doc-drift-map.md +49 -0
- package/skills/product-spec/SKILL.md +244 -0
- package/templates/env.example +5 -0
- package/templates/routines/nightly-memory-digest.yaml +10 -0
- package/templates/workspace/AGENTS.md +26 -0
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {
|
|
5
|
+
conversationKey,
|
|
6
|
+
ensureDir,
|
|
7
|
+
nowIso,
|
|
8
|
+
randomId,
|
|
9
|
+
readJsonFile,
|
|
10
|
+
sanitizeSegment,
|
|
11
|
+
writeJsonFile,
|
|
12
|
+
} from './utils.ts';
|
|
13
|
+
import type {
|
|
14
|
+
WorkSessionActiveSurface,
|
|
15
|
+
WorkSessionAssistantIndex,
|
|
16
|
+
WorkSessionCliResumeRefType,
|
|
17
|
+
WorkSessionDeliveryIndex,
|
|
18
|
+
WorkSessionMeta,
|
|
19
|
+
WorkSessionOrigin,
|
|
20
|
+
WorkSessionOwnershipSource,
|
|
21
|
+
WorkSessionResumeCapability,
|
|
22
|
+
} from './types.ts';
|
|
23
|
+
|
|
24
|
+
interface CreateWorkSessionInput {
|
|
25
|
+
workSessionId?: string;
|
|
26
|
+
threadHandle?: string | null;
|
|
27
|
+
assistantName: string;
|
|
28
|
+
assistantCodexHome?: string | null;
|
|
29
|
+
workspaceRoot: string;
|
|
30
|
+
runtimeThreadId: string;
|
|
31
|
+
cliResumeRef?: string | null;
|
|
32
|
+
cliResumeRefType?: WorkSessionCliResumeRefType;
|
|
33
|
+
threadName?: string | null;
|
|
34
|
+
origin: WorkSessionOrigin;
|
|
35
|
+
deliveryChannel?: string | null;
|
|
36
|
+
deliveryConversationKey?: string | null;
|
|
37
|
+
activeSurface: WorkSessionActiveSurface;
|
|
38
|
+
ownershipSource: WorkSessionOwnershipSource;
|
|
39
|
+
resumeCapability?: WorkSessionResumeCapability;
|
|
40
|
+
lastRuntimeActivityAt?: string | null;
|
|
41
|
+
lastSurfaceSwitchAt?: string;
|
|
42
|
+
createdAt?: string;
|
|
43
|
+
updatedAt?: string;
|
|
44
|
+
archivedAt?: string | null;
|
|
45
|
+
lastTurnId?: string | null;
|
|
46
|
+
lastPromptPreview?: string | null;
|
|
47
|
+
lastReplyPreview?: string | null;
|
|
48
|
+
lastHandoffExportAt?: string | null;
|
|
49
|
+
lastAttachAt?: string | null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type WorkSessionUpdate = Partial<Omit<WorkSessionMeta,
|
|
53
|
+
'workSessionId'
|
|
54
|
+
| 'threadHandle'
|
|
55
|
+
| 'assistantName'
|
|
56
|
+
| 'assistantCodexHome'
|
|
57
|
+
| 'workspaceRoot'
|
|
58
|
+
| 'runtime'
|
|
59
|
+
| 'runtimeThreadId'
|
|
60
|
+
| 'createdAt'
|
|
61
|
+
>>;
|
|
62
|
+
|
|
63
|
+
function normalizeNullableText(value: string | null | undefined): string | null {
|
|
64
|
+
if (value === undefined || value === null) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
const trimmed = String(value).trim();
|
|
68
|
+
return trimmed || null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function normalizeThreadHandle(value: string | null | undefined): string | null {
|
|
72
|
+
const normalized = normalizeNullableText(value)?.toLowerCase() || null;
|
|
73
|
+
if (!normalized) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
if (!/^[a-z0-9][a-z0-9-]{2,31}$/.test(normalized)) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
return normalized;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function ensureDeliveryBinding(channel: string | null, key: string | null): void {
|
|
83
|
+
if ((channel && !key) || (!channel && key)) {
|
|
84
|
+
throw new Error('deliveryChannel and deliveryConversationKey must be set together');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function resolveResumeCapability(
|
|
89
|
+
requested: WorkSessionResumeCapability | undefined,
|
|
90
|
+
cliResumeRef: string | null,
|
|
91
|
+
): WorkSessionResumeCapability {
|
|
92
|
+
if (requested === 'ready' && !cliResumeRef) {
|
|
93
|
+
throw new Error('resumeCapability=ready requires cliResumeRef');
|
|
94
|
+
}
|
|
95
|
+
if (requested) {
|
|
96
|
+
return requested;
|
|
97
|
+
}
|
|
98
|
+
return cliResumeRef ? 'ready' : 'not_ready';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export class WorkSessionStore {
|
|
102
|
+
private readonly rootDir: string;
|
|
103
|
+
|
|
104
|
+
constructor(rootDir: string) {
|
|
105
|
+
this.rootDir = ensureDir(rootDir);
|
|
106
|
+
ensureDir(path.join(this.rootDir, 'objects'));
|
|
107
|
+
ensureDir(path.join(this.rootDir, 'indexes', 'assistants'));
|
|
108
|
+
ensureDir(path.join(this.rootDir, 'indexes', 'deliveries'));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private workSessionDir(workSessionId: string): string {
|
|
112
|
+
return path.join(this.rootDir, 'objects', sanitizeSegment(workSessionId));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private metaFile(workSessionId: string): string {
|
|
116
|
+
return path.join(this.workSessionDir(workSessionId), 'meta.json');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private assistantIndexFile(assistantName: string): string {
|
|
120
|
+
return path.join(this.rootDir, 'indexes', 'assistants', `${sanitizeSegment(assistantName)}.json`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private deliveryIndexFile(deliveryChannel: string, deliveryConversationKey: string): string {
|
|
124
|
+
return path.join(this.rootDir, 'indexes', 'deliveries', `${conversationKey(deliveryChannel, deliveryConversationKey)}.json`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private listRawWorkSessions(): WorkSessionMeta[] {
|
|
128
|
+
const objectsDir = path.join(this.rootDir, 'objects');
|
|
129
|
+
if (!fs.existsSync(objectsDir)) {
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
return fs.readdirSync(objectsDir, { withFileTypes: true })
|
|
133
|
+
.filter((entry) => entry.isDirectory())
|
|
134
|
+
.map((entry) => readJsonFile(path.join(objectsDir, entry.name, 'meta.json')) as WorkSessionMeta | null)
|
|
135
|
+
.filter((entry): entry is WorkSessionMeta => Boolean(entry));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private threadHandleOwner(assistantName: string, threadHandle: string): string | null {
|
|
139
|
+
for (const item of this.listRawWorkSessions()) {
|
|
140
|
+
if (item.assistantName !== assistantName) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (normalizeThreadHandle(item.threadHandle) === threadHandle) {
|
|
144
|
+
return item.workSessionId;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private generateThreadHandle(assistantName: string, workSessionId: string, preferred?: string | null): string {
|
|
151
|
+
const normalizedPreferred = normalizeThreadHandle(preferred);
|
|
152
|
+
if (normalizedPreferred) {
|
|
153
|
+
const owner = this.threadHandleOwner(assistantName, normalizedPreferred);
|
|
154
|
+
if (!owner || owner === workSessionId) {
|
|
155
|
+
return normalizedPreferred;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const digest = crypto.createHash('sha1').update(`${assistantName}:${workSessionId}`).digest('hex');
|
|
160
|
+
for (const length of [7, 9, 11, 13]) {
|
|
161
|
+
const candidate = `t${digest.slice(0, length)}`;
|
|
162
|
+
const owner = this.threadHandleOwner(assistantName, candidate);
|
|
163
|
+
if (!owner || owner === workSessionId) {
|
|
164
|
+
return candidate;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
for (let counter = 1; counter < 1000; counter += 1) {
|
|
169
|
+
const candidate = `t${digest.slice(0, 10)}-${counter}`;
|
|
170
|
+
const owner = this.threadHandleOwner(assistantName, candidate);
|
|
171
|
+
if (!owner || owner === workSessionId) {
|
|
172
|
+
return candidate;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
throw new Error(`unable to generate unique threadHandle for assistant ${assistantName}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private normalizeLoadedMeta(raw: WorkSessionMeta | null): WorkSessionMeta | null {
|
|
180
|
+
if (!raw) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
const expectedHandle = this.generateThreadHandle(raw.assistantName, raw.workSessionId, raw.threadHandle);
|
|
184
|
+
if (raw.threadHandle === expectedHandle) {
|
|
185
|
+
return raw;
|
|
186
|
+
}
|
|
187
|
+
const next: WorkSessionMeta = {
|
|
188
|
+
...raw,
|
|
189
|
+
threadHandle: expectedHandle,
|
|
190
|
+
};
|
|
191
|
+
this.writeMeta(next);
|
|
192
|
+
return next;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private normalizeCreateInput(input: CreateWorkSessionInput): WorkSessionMeta {
|
|
196
|
+
const workSessionId = normalizeNullableText(input.workSessionId) || randomId('work_session');
|
|
197
|
+
const assistantName = normalizeNullableText(input.assistantName);
|
|
198
|
+
const workspaceRoot = normalizeNullableText(input.workspaceRoot);
|
|
199
|
+
const runtimeThreadId = normalizeNullableText(input.runtimeThreadId);
|
|
200
|
+
const cliResumeRef = normalizeNullableText(input.cliResumeRef);
|
|
201
|
+
const assistantCodexHome = normalizeNullableText(input.assistantCodexHome);
|
|
202
|
+
const deliveryChannel = normalizeNullableText(input.deliveryChannel);
|
|
203
|
+
const deliveryConversationKey = normalizeNullableText(input.deliveryConversationKey);
|
|
204
|
+
ensureDeliveryBinding(deliveryChannel, deliveryConversationKey);
|
|
205
|
+
if (!assistantName) {
|
|
206
|
+
throw new Error('assistantName is required');
|
|
207
|
+
}
|
|
208
|
+
if (!workspaceRoot) {
|
|
209
|
+
throw new Error('workspaceRoot is required');
|
|
210
|
+
}
|
|
211
|
+
if (!runtimeThreadId) {
|
|
212
|
+
throw new Error('runtimeThreadId is required');
|
|
213
|
+
}
|
|
214
|
+
const createdAt = input.createdAt || nowIso();
|
|
215
|
+
const updatedAt = input.updatedAt || createdAt;
|
|
216
|
+
return {
|
|
217
|
+
workSessionId,
|
|
218
|
+
threadHandle: this.generateThreadHandle(assistantName, workSessionId, input.threadHandle),
|
|
219
|
+
assistantName,
|
|
220
|
+
assistantCodexHome,
|
|
221
|
+
workspaceRoot,
|
|
222
|
+
runtime: 'codex',
|
|
223
|
+
runtimeThreadId,
|
|
224
|
+
cliResumeRef,
|
|
225
|
+
cliResumeRefType: input.cliResumeRefType || (cliResumeRef ? 'unknown' : 'null'),
|
|
226
|
+
threadName: normalizeNullableText(input.threadName),
|
|
227
|
+
origin: input.origin,
|
|
228
|
+
deliveryChannel,
|
|
229
|
+
deliveryConversationKey,
|
|
230
|
+
activeSurface: input.activeSurface,
|
|
231
|
+
ownershipSource: input.ownershipSource,
|
|
232
|
+
resumeCapability: resolveResumeCapability(input.resumeCapability, cliResumeRef),
|
|
233
|
+
lastRuntimeActivityAt: normalizeNullableText(input.lastRuntimeActivityAt),
|
|
234
|
+
lastSurfaceSwitchAt: input.lastSurfaceSwitchAt || updatedAt,
|
|
235
|
+
createdAt,
|
|
236
|
+
updatedAt,
|
|
237
|
+
archivedAt: normalizeNullableText(input.archivedAt),
|
|
238
|
+
lastTurnId: normalizeNullableText(input.lastTurnId),
|
|
239
|
+
lastPromptPreview: normalizeNullableText(input.lastPromptPreview),
|
|
240
|
+
lastReplyPreview: normalizeNullableText(input.lastReplyPreview),
|
|
241
|
+
lastHandoffExportAt: normalizeNullableText(input.lastHandoffExportAt),
|
|
242
|
+
lastAttachAt: normalizeNullableText(input.lastAttachAt),
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private writeMeta(meta: WorkSessionMeta): void {
|
|
247
|
+
writeJsonFile(this.metaFile(meta.workSessionId), meta);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private loadAssistantIndex(assistantName: string): WorkSessionAssistantIndex | null {
|
|
251
|
+
return readJsonFile(this.assistantIndexFile(assistantName)) as WorkSessionAssistantIndex | null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private loadDeliveryIndex(deliveryChannel: string, deliveryConversationKey: string): WorkSessionDeliveryIndex | null {
|
|
255
|
+
return readJsonFile(this.deliveryIndexFile(deliveryChannel, deliveryConversationKey)) as WorkSessionDeliveryIndex | null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
createWorkSession(input: CreateWorkSessionInput): WorkSessionMeta {
|
|
259
|
+
const meta = this.normalizeCreateInput(input);
|
|
260
|
+
if (this.getWorkSession(meta.workSessionId)) {
|
|
261
|
+
throw new Error(`work_session already exists: ${meta.workSessionId}`);
|
|
262
|
+
}
|
|
263
|
+
this.writeMeta(meta);
|
|
264
|
+
this.setAssistantActiveWorkSession(meta.assistantName, meta.workSessionId, meta.updatedAt);
|
|
265
|
+
if (meta.deliveryChannel && meta.deliveryConversationKey) {
|
|
266
|
+
this.setDeliveryActiveWorkSession(meta.deliveryChannel, meta.deliveryConversationKey, meta.workSessionId, meta.updatedAt);
|
|
267
|
+
}
|
|
268
|
+
return meta;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
getWorkSession(workSessionId: string): WorkSessionMeta | null {
|
|
272
|
+
return this.normalizeLoadedMeta(readJsonFile(this.metaFile(workSessionId)) as WorkSessionMeta | null);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
listWorkSessions(): WorkSessionMeta[] {
|
|
276
|
+
return this.listRawWorkSessions()
|
|
277
|
+
.map((entry) => this.normalizeLoadedMeta(entry))
|
|
278
|
+
.filter((entry): entry is WorkSessionMeta => Boolean(entry))
|
|
279
|
+
.sort((left, right) => String(right.updatedAt).localeCompare(String(left.updatedAt)));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
listWorkSessionsForAssistant(assistantName: string, options: { includeArchived?: boolean } = {}): WorkSessionMeta[] {
|
|
283
|
+
return this.listWorkSessions().filter((item) => (
|
|
284
|
+
item.assistantName === assistantName
|
|
285
|
+
&& (options.includeArchived ? true : !item.archivedAt)
|
|
286
|
+
));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
findWorkSessionByThreadHandle(assistantName: string, threadHandle: string): WorkSessionMeta | null {
|
|
290
|
+
const normalized = normalizeThreadHandle(threadHandle);
|
|
291
|
+
if (!normalized) {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
return this.listWorkSessionsForAssistant(assistantName, { includeArchived: true })
|
|
295
|
+
.find((item) => item.threadHandle === normalized) || null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
rebindRuntimeThread(workSessionId: string, runtimeThreadId: string, updatedAt = nowIso()): WorkSessionMeta {
|
|
299
|
+
const current = this.getWorkSession(workSessionId);
|
|
300
|
+
if (!current) {
|
|
301
|
+
throw new Error(`work_session not found: ${workSessionId}`);
|
|
302
|
+
}
|
|
303
|
+
const nextRuntimeThreadId = normalizeNullableText(runtimeThreadId);
|
|
304
|
+
if (!nextRuntimeThreadId) {
|
|
305
|
+
throw new Error('runtimeThreadId is required');
|
|
306
|
+
}
|
|
307
|
+
if (current.runtimeThreadId === nextRuntimeThreadId) {
|
|
308
|
+
return current;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const shouldFollowRuntimeThread = (
|
|
312
|
+
current.cliResumeRefType === 'session_id'
|
|
313
|
+
&& (!current.cliResumeRef || current.cliResumeRef === current.runtimeThreadId)
|
|
314
|
+
);
|
|
315
|
+
const nextCliResumeRef = shouldFollowRuntimeThread ? nextRuntimeThreadId : current.cliResumeRef;
|
|
316
|
+
const nextCliResumeRefType = shouldFollowRuntimeThread ? 'session_id' : current.cliResumeRefType;
|
|
317
|
+
const nextResumeCapability = resolveResumeCapability(
|
|
318
|
+
shouldFollowRuntimeThread ? 'ready' : current.resumeCapability,
|
|
319
|
+
nextCliResumeRef,
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
const next: WorkSessionMeta = {
|
|
323
|
+
...current,
|
|
324
|
+
runtimeThreadId: nextRuntimeThreadId,
|
|
325
|
+
cliResumeRef: nextCliResumeRef,
|
|
326
|
+
cliResumeRefType: nextCliResumeRefType,
|
|
327
|
+
resumeCapability: nextResumeCapability,
|
|
328
|
+
updatedAt,
|
|
329
|
+
};
|
|
330
|
+
this.writeMeta(next);
|
|
331
|
+
return next;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
updateWorkSession(workSessionId: string, patch: WorkSessionUpdate): WorkSessionMeta {
|
|
335
|
+
const current = this.getWorkSession(workSessionId);
|
|
336
|
+
if (!current) {
|
|
337
|
+
throw new Error(`work_session not found: ${workSessionId}`);
|
|
338
|
+
}
|
|
339
|
+
if ('activeSurface' in patch !== 'ownershipSource' in patch) {
|
|
340
|
+
throw new Error('activeSurface and ownershipSource must be updated together');
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const nextDeliveryChannel = 'deliveryChannel' in patch
|
|
344
|
+
? normalizeNullableText(patch.deliveryChannel)
|
|
345
|
+
: current.deliveryChannel;
|
|
346
|
+
const nextDeliveryConversationKey = 'deliveryConversationKey' in patch
|
|
347
|
+
? normalizeNullableText(patch.deliveryConversationKey)
|
|
348
|
+
: current.deliveryConversationKey;
|
|
349
|
+
ensureDeliveryBinding(nextDeliveryChannel, nextDeliveryConversationKey);
|
|
350
|
+
if (current.deliveryChannel && current.deliveryConversationKey) {
|
|
351
|
+
if (nextDeliveryChannel !== current.deliveryChannel || nextDeliveryConversationKey !== current.deliveryConversationKey) {
|
|
352
|
+
throw new Error('delivery binding is immutable once linked');
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const nextCliResumeRef = 'cliResumeRef' in patch
|
|
357
|
+
? normalizeNullableText(patch.cliResumeRef)
|
|
358
|
+
: current.cliResumeRef;
|
|
359
|
+
const nextResumeCapability = resolveResumeCapability(patch.resumeCapability, nextCliResumeRef);
|
|
360
|
+
const updatedAt = patch.updatedAt || nowIso();
|
|
361
|
+
const next: WorkSessionMeta = {
|
|
362
|
+
...current,
|
|
363
|
+
...patch,
|
|
364
|
+
cliResumeRef: nextCliResumeRef,
|
|
365
|
+
deliveryChannel: nextDeliveryChannel,
|
|
366
|
+
deliveryConversationKey: nextDeliveryConversationKey,
|
|
367
|
+
resumeCapability: nextResumeCapability,
|
|
368
|
+
updatedAt,
|
|
369
|
+
};
|
|
370
|
+
this.writeMeta(next);
|
|
371
|
+
return next;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
bindDeliveryWorkSession(
|
|
375
|
+
workSessionId: string,
|
|
376
|
+
deliveryChannel: string,
|
|
377
|
+
deliveryConversationKey: string,
|
|
378
|
+
updatedAt = nowIso(),
|
|
379
|
+
): WorkSessionMeta {
|
|
380
|
+
const current = this.getWorkSession(workSessionId);
|
|
381
|
+
if (!current) {
|
|
382
|
+
throw new Error(`work_session not found: ${workSessionId}`);
|
|
383
|
+
}
|
|
384
|
+
if (current.deliveryChannel === deliveryChannel && current.deliveryConversationKey === deliveryConversationKey) {
|
|
385
|
+
this.setDeliveryActiveWorkSession(deliveryChannel, deliveryConversationKey, current.workSessionId, updatedAt);
|
|
386
|
+
return current;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const previousChannel = current.deliveryChannel;
|
|
390
|
+
const previousConversationKey = current.deliveryConversationKey;
|
|
391
|
+
const next: WorkSessionMeta = {
|
|
392
|
+
...current,
|
|
393
|
+
deliveryChannel,
|
|
394
|
+
deliveryConversationKey,
|
|
395
|
+
updatedAt,
|
|
396
|
+
};
|
|
397
|
+
this.writeMeta(next);
|
|
398
|
+
|
|
399
|
+
if (previousChannel && previousConversationKey) {
|
|
400
|
+
const previousIndex = this.getDeliveryIndex(previousChannel, previousConversationKey);
|
|
401
|
+
if (previousIndex?.activeWorkSessionId === current.workSessionId) {
|
|
402
|
+
this.setDeliveryActiveWorkSession(previousChannel, previousConversationKey, null, updatedAt);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
this.setDeliveryActiveWorkSession(deliveryChannel, deliveryConversationKey, next.workSessionId, updatedAt);
|
|
407
|
+
return next;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
getAssistantIndex(assistantName: string): WorkSessionAssistantIndex | null {
|
|
411
|
+
return this.loadAssistantIndex(assistantName);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
getActiveWorkSessionForAssistant(assistantName: string): WorkSessionMeta | null {
|
|
415
|
+
const index = this.loadAssistantIndex(assistantName);
|
|
416
|
+
return index?.activeWorkSessionId ? this.getWorkSession(index.activeWorkSessionId) : null;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
setAssistantActiveWorkSession(assistantName: string, workSessionId: string | null, updatedAt = nowIso()): WorkSessionAssistantIndex {
|
|
420
|
+
if (workSessionId) {
|
|
421
|
+
const meta = this.getWorkSession(workSessionId);
|
|
422
|
+
if (!meta) {
|
|
423
|
+
throw new Error(`work_session not found: ${workSessionId}`);
|
|
424
|
+
}
|
|
425
|
+
if (meta.assistantName !== assistantName) {
|
|
426
|
+
throw new Error('assistant index cannot point to another assistant\'s work_session');
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
const index: WorkSessionAssistantIndex = {
|
|
430
|
+
assistantName,
|
|
431
|
+
activeWorkSessionId: workSessionId,
|
|
432
|
+
updatedAt,
|
|
433
|
+
};
|
|
434
|
+
writeJsonFile(this.assistantIndexFile(assistantName), index);
|
|
435
|
+
return index;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
getDeliveryIndex(deliveryChannel: string, deliveryConversationKey: string): WorkSessionDeliveryIndex | null {
|
|
439
|
+
return this.loadDeliveryIndex(deliveryChannel, deliveryConversationKey);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
getActiveWorkSessionForDelivery(deliveryChannel: string, deliveryConversationKey: string): WorkSessionMeta | null {
|
|
443
|
+
const index = this.loadDeliveryIndex(deliveryChannel, deliveryConversationKey);
|
|
444
|
+
return index?.activeWorkSessionId ? this.getWorkSession(index.activeWorkSessionId) : null;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
setDeliveryActiveWorkSession(
|
|
448
|
+
deliveryChannel: string,
|
|
449
|
+
deliveryConversationKey: string,
|
|
450
|
+
workSessionId: string | null,
|
|
451
|
+
updatedAt = nowIso(),
|
|
452
|
+
): WorkSessionDeliveryIndex {
|
|
453
|
+
if (workSessionId) {
|
|
454
|
+
const meta = this.getWorkSession(workSessionId);
|
|
455
|
+
if (!meta) {
|
|
456
|
+
throw new Error(`work_session not found: ${workSessionId}`);
|
|
457
|
+
}
|
|
458
|
+
if (meta.deliveryChannel !== deliveryChannel || meta.deliveryConversationKey !== deliveryConversationKey) {
|
|
459
|
+
throw new Error('delivery index cannot point to an unlinked work_session');
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
const index: WorkSessionDeliveryIndex = {
|
|
463
|
+
deliveryChannel,
|
|
464
|
+
deliveryConversationKey,
|
|
465
|
+
activeWorkSessionId: workSessionId,
|
|
466
|
+
updatedAt,
|
|
467
|
+
};
|
|
468
|
+
writeJsonFile(this.deliveryIndexFile(deliveryChannel, deliveryConversationKey), index);
|
|
469
|
+
return index;
|
|
470
|
+
}
|
|
471
|
+
}
|
package/docs/.gitkeep
ADDED
|
File without changes
|