@ours.network/fleet 1.2.0-nightly.19 → 1.2.0-nightly.20
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 +10 -1
- package/dist/agent-ours/correspondence.d.ts +10 -0
- package/dist/agent-ours/correspondence.js +1 -0
- package/dist/agent-ours/runtime.d.ts +6 -0
- package/dist/agent-ours/runtime.js +36 -0
- package/dist/agent-ours/service.js +2 -1
- package/dist/application/conversation-history.d.ts +6 -0
- package/dist/application/conversation-history.js +65 -0
- package/dist/application/fleet-query-service.d.ts +1 -1
- package/dist/application/fleet-query-service.js +2 -2
- package/dist/application/preset-provenance.d.ts +19 -0
- package/dist/application/preset-provenance.js +59 -0
- package/dist/application/room-layout-definitions.d.ts +45 -0
- package/dist/application/room-layout-definitions.js +130 -0
- package/dist/application/session-control.d.ts +8 -0
- package/dist/application/session-control.js +21 -3
- package/dist/application/task-layouts.d.ts +89 -0
- package/dist/application/task-layouts.js +248 -0
- package/dist/application/task-room-service.d.ts +82 -0
- package/dist/application/task-room-service.js +197 -9
- package/dist/build-info.json +4 -4
- package/dist/cli.js +2 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +7 -0
- package/dist/init-wizard.js +4 -2
- package/dist/rooms-tasks/add-member.d.ts +48 -0
- package/dist/rooms-tasks/add-member.js +83 -0
- package/dist/rooms-tasks/cli.js +20 -0
- package/dist/rooms-tasks/external-worker.d.ts +1 -1
- package/dist/rooms-tasks/external-worker.js +3 -3
- package/dist/rooms-tasks/layout-cli.d.ts +2 -0
- package/dist/rooms-tasks/layout-cli.js +81 -0
- package/dist/rooms-tasks/layout-config.d.ts +11 -0
- package/dist/rooms-tasks/layout-config.js +89 -0
- package/dist/rooms-tasks/layout-control.d.ts +21 -0
- package/dist/rooms-tasks/layout-control.js +52 -0
- package/dist/rooms-tasks/layout-service.d.ts +39 -0
- package/dist/rooms-tasks/layout-service.js +173 -0
- package/dist/rooms-tasks/layout.d.ts +102 -0
- package/dist/rooms-tasks/layout.js +227 -0
- package/dist/rooms-tasks/live-readiness.js +2 -1
- package/dist/rooms-tasks/provision.d.ts +3 -0
- package/dist/rooms-tasks/provision.js +44 -22
- package/dist/rooms-tasks/room-state.d.ts +1 -1
- package/dist/rooms-tasks/room-state.js +3 -1
- package/dist/rooms-tasks/task-state.d.ts +19 -0
- package/dist/rooms-tasks/task-state.js +58 -6
- package/dist/rooms-tasks/types.d.ts +14 -0
- package/dist/runner.d.ts +7 -2
- package/dist/runner.js +52 -4
- package/dist/session/acp.d.ts +3 -0
- package/dist/session/acp.js +7 -0
- package/dist/session/arbiter.d.ts +3 -0
- package/dist/session/arbiter.js +20 -2
- package/dist/session/codex-app-server.d.ts +3 -0
- package/dist/session/codex-app-server.js +25 -1
- package/dist/session/control.d.ts +10 -1
- package/dist/session/control.js +36 -2
- package/dist/session/conversation-normalizer.d.ts +2 -1
- package/dist/session/conversation-normalizer.js +1 -1
- package/dist/session/conversation-types.d.ts +5 -3
- package/dist/session/task-notice.d.ts +14 -0
- package/dist/session/task-notice.js +13 -0
- package/dist/session/types.d.ts +3 -0
- package/dist/socket-path.d.ts +2 -0
- package/dist/socket-path.js +22 -0
- package/dist/temp-idle.d.ts +17 -0
- package/dist/temp-idle.js +42 -0
- package/dist/temp-lifecycle.d.ts +1 -1
- package/dist/temp-lifecycle.js +1 -1
- package/dist/web/agent-attachments.d.ts +29 -0
- package/dist/web/agent-attachments.js +162 -0
- package/dist/web/auth.d.ts +4 -1
- package/dist/web/auth.js +23 -2
- package/dist/web/control.js +2 -1
- package/dist/web/fleet-config-service.d.ts +4 -1
- package/dist/web/fleet-config-service.js +46 -15
- package/dist/web/prefix-gateway.d.ts +20 -0
- package/dist/web/prefix-gateway.js +150 -0
- package/dist/web/runtime.d.ts +4 -0
- package/dist/web/runtime.js +19 -14
- package/dist/web/server.d.ts +5 -0
- package/dist/web/server.js +275 -24
- package/docs/room-layouts.md +92 -0
- package/examples/room-layouts/fresh.yaml +27 -0
- package/examples/room-layouts/shared.yaml +19 -0
- package/package.json +14 -13
- package/presets/fleet/room_layouts/engineering.yaml +25 -0
- package/presets/fleet/room_layouts/pair.yaml +24 -0
- package/presets/fleet/room_layouts/single.yaml +17 -0
- package/presets/fleet/room_layouts/team.yaml +30 -0
- package/dist/web-app/assets/index-59GF-gsZ.css +0 -1
- package/dist/web-app/assets/index-C0h3ALvs.js +0 -10
- package/dist/web-app/icons/ours-fleet-maskable.svg +0 -4
- package/dist/web-app/icons/ours-fleet.svg +0 -4
- package/dist/web-app/index.html +0 -17
- package/dist/web-app/manifest.webmanifest +0 -15
- package/dist/web-app/offline.html +0 -18
- package/dist/web-app/sw.js +0 -51
package/dist/runner.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createLayoutControl } from './rooms-tasks/layout-control.js';
|
|
2
|
+
import { TemporaryChatIdle } from './temp-idle.js';
|
|
1
3
|
import { prepareManagedAgent, releaseManagedAgent } from './agent-ours/service.js';
|
|
2
4
|
import { prepareManagedHarness } from './agent-ours/harness.js';
|
|
3
5
|
import { existsSync, readFileSync, writeFileSync, rmSync, mkdirSync, realpathSync } from 'node:fs';
|
|
@@ -75,12 +77,16 @@ const defaultDeps = () => ({
|
|
|
75
77
|
});
|
|
76
78
|
const MONITOR_OWNER_FILE = '.monitor-owner';
|
|
77
79
|
const OBSOLETE_OURS_AUTOSTART_ENV = 'OURS_AUTOSTART';
|
|
78
|
-
function localFleetAuditor(stateDir, caller, log) {
|
|
80
|
+
export function localFleetAuditor(stateDir, caller, log, session) {
|
|
81
|
+
const bindings = new Map();
|
|
79
82
|
const store = new FleetCommandAuditStore(join(stateDir, '.fleet-command-audit.json'));
|
|
80
83
|
return {
|
|
81
84
|
async begin(requestId, argv) {
|
|
82
85
|
let attempt = store.begin(requestId, caller, argv);
|
|
83
86
|
if (attempt.invocation === 'sending') {
|
|
87
|
+
const binding = session.taskNoticeBinding?.();
|
|
88
|
+
if (binding)
|
|
89
|
+
bindings.set(attempt.correlationId, binding);
|
|
84
90
|
log(`[${caller}] fleet proxy command ${attempt.correlationId} `
|
|
85
91
|
+ `route=${attempt.classification.route} decision=${attempt.classification.decision}`);
|
|
86
92
|
attempt = store.invocation(attempt.correlationId, caller, 'delivered');
|
|
@@ -94,6 +100,19 @@ function localFleetAuditor(stateDir, caller, log) {
|
|
|
94
100
|
...(input.resourceIds ? { resourceIds: input.resourceIds } : {}),
|
|
95
101
|
...(input.presentations ? { presentations: input.presentations } : {}),
|
|
96
102
|
});
|
|
103
|
+
const binding = bindings.get(attempt.correlationId);
|
|
104
|
+
if (binding && attempt.outcome?.class === 'success' && attempt.outcome.effect === 'completed') {
|
|
105
|
+
for (const p of attempt.outcome.presentations ?? [])
|
|
106
|
+
if (p.kind === 'task' && p.operation === 'create') {
|
|
107
|
+
try {
|
|
108
|
+
session.recordTaskCreated?.(binding, { operationId: p.eventId, taskId: p.id, title: p.title ?? p.id, state: p.newState });
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
log(`[${caller}] task-created conversation notice could not be persisted`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
bindings.delete(attempt.correlationId);
|
|
115
|
+
}
|
|
97
116
|
if (attempt.outcome?.delivery === 'sending')
|
|
98
117
|
attempt = store.outcome(attempt.correlationId, caller, 'delivered');
|
|
99
118
|
return attempt;
|
|
@@ -539,6 +558,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
539
558
|
wrappedArgv = [...rprefix, ...wrappedArgv];
|
|
540
559
|
}
|
|
541
560
|
const managedService = await deps.prepareAgentOurs(role, dir, opts.identityLifetime ? opts.identityLifetime === 'temporary' : temp);
|
|
561
|
+
let layoutRetiring = false;
|
|
542
562
|
try {
|
|
543
563
|
const managedHarness = prepareManagedHarness(role, dir, runCwd, managedService.descriptor, harnessChildEnv(role, launch.env, dir));
|
|
544
564
|
// Start-stagger: space this launch at least start_stagger_ms after the previous
|
|
@@ -670,11 +690,18 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
670
690
|
...(configPath ? { configPath } : {}),
|
|
671
691
|
});
|
|
672
692
|
control = deps.createControlServer(dir, arbiter, deps.log);
|
|
693
|
+
control.setCorrespondence?.({ contacts: () => managedService.runtime.readContacts(), history: query => managedService.runtime.readHistory(query) });
|
|
673
694
|
control.setFleetAuditor(ownerChannel ? {
|
|
674
695
|
begin: (requestId, argv) => ownerChannel.beginFleetCommandAudit(requestId, argv),
|
|
675
696
|
finish: input => ownerChannel.finishFleetCommandAudit(input),
|
|
676
697
|
present: presentations => ownerChannel.notifyFleetLifecycle(presentations),
|
|
677
|
-
} : localFleetAuditor(dir, name, deps.log));
|
|
698
|
+
} : localFleetAuditor(dir, name, deps.log, agentSession));
|
|
699
|
+
control.setLayoutControl?.(createLayoutControl({
|
|
700
|
+
agent: name, temporary: temp, standalone: !role.roomMemberStartup,
|
|
701
|
+
runtime: managedService.runtime, session: arbiter,
|
|
702
|
+
stopping: () => Boolean(deps.shouldStop?.()),
|
|
703
|
+
retire: async () => { layoutRetiring = true; await arbiter.close(); },
|
|
704
|
+
}));
|
|
678
705
|
await control.start();
|
|
679
706
|
}
|
|
680
707
|
catch (error) {
|
|
@@ -902,6 +929,14 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
902
929
|
}
|
|
903
930
|
},
|
|
904
931
|
});
|
|
932
|
+
let chatIdle;
|
|
933
|
+
try {
|
|
934
|
+
if (temp && !role.roomMemberStartup)
|
|
935
|
+
chatIdle = new TemporaryChatIdle(dir, deps.now());
|
|
936
|
+
}
|
|
937
|
+
catch {
|
|
938
|
+
deps.log(`[${name}] automatic idle closure disabled: activity state is unreadable`);
|
|
939
|
+
}
|
|
905
940
|
const start = deps.now();
|
|
906
941
|
let nextLoopReloadAt = deps.now() + 30_000;
|
|
907
942
|
let nextIdentityPollAt = deps.now();
|
|
@@ -925,6 +960,19 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
925
960
|
void recoveryController.recover(observation.generation).catch(error => deps.log(`[${name}] daemon recovery controller failed: ${error?.name ?? 'Error'}`));
|
|
926
961
|
}
|
|
927
962
|
}
|
|
963
|
+
let idleExpired = false;
|
|
964
|
+
try {
|
|
965
|
+
idleExpired = chatIdle?.observe(sessionHandle.snapshot(), sessionHandle.conversationSnapshot?.().queueDepth ?? -1, now) ?? false;
|
|
966
|
+
}
|
|
967
|
+
catch {
|
|
968
|
+
chatIdle = undefined;
|
|
969
|
+
deps.log(`[${name}] automatic idle closure disabled: activity state could not be saved`);
|
|
970
|
+
}
|
|
971
|
+
if (idleExpired && await arbiter.retireIfIdle((snapshot, queue) => chatIdle.observe(snapshot, queue, deps.now()))) {
|
|
972
|
+
retirementReason = 'idle-timeout';
|
|
973
|
+
sessionClosed = true;
|
|
974
|
+
break;
|
|
975
|
+
}
|
|
928
976
|
if (deps.shouldStop?.()) {
|
|
929
977
|
if (temp) {
|
|
930
978
|
retirementReason = requestedTempStopReason(dir) ?? 'supervisor-signal';
|
|
@@ -1053,11 +1101,11 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
1053
1101
|
throw new SupervisorRecycleRequiredError();
|
|
1054
1102
|
return {
|
|
1055
1103
|
elapsedSecs: elapsed, exit: exitRecord, rotated, mode, modelRecovery,
|
|
1056
|
-
...(retirementReason ? { retirementReason } : {}),
|
|
1104
|
+
...(layoutRetiring ? { retirementReason: 'operator-stop' } : retirementReason ? { retirementReason } : {}),
|
|
1057
1105
|
};
|
|
1058
1106
|
}
|
|
1059
1107
|
finally {
|
|
1060
|
-
await managedService.close(
|
|
1108
|
+
await managedService.close(layoutRetiring);
|
|
1061
1109
|
}
|
|
1062
1110
|
}
|
|
1063
1111
|
/**
|
package/dist/session/acp.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type TaskNoticeBinding, type TaskCreatedNotice } from './task-notice.js';
|
|
1
2
|
import * as acp from '@agentclientprotocol/sdk';
|
|
2
3
|
import type { CommonPermissions } from '../config.js';
|
|
3
4
|
import type { AcpMcpServer } from '../harness/types.js';
|
|
@@ -340,6 +341,8 @@ export declare class AcpSession implements AgentSession {
|
|
|
340
341
|
private codexMessagePhase;
|
|
341
342
|
/** Normalize every ACP update losslessly into the durable ledger. */
|
|
342
343
|
private recordConversationUpdate;
|
|
344
|
+
taskNoticeBinding(): TaskNoticeBinding | undefined;
|
|
345
|
+
recordTaskCreated(binding: TaskNoticeBinding, notice: TaskCreatedNotice): void;
|
|
343
346
|
conversationPage(request?: {
|
|
344
347
|
after?: string;
|
|
345
348
|
limit?: number;
|
package/dist/session/acp.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { appendTaskCreated } from './task-notice.js';
|
|
1
2
|
import { spawn } from 'node:child_process';
|
|
2
3
|
import { createHash, randomUUID } from 'node:crypto';
|
|
3
4
|
import { existsSync, lstatSync, readFileSync, readlinkSync, realpathSync, writeFileSync, } from 'node:fs';
|
|
@@ -760,6 +761,8 @@ export class AcpSession {
|
|
|
760
761
|
const existing = this.conversation.receiptFor(command.commandId, bodyDigest);
|
|
761
762
|
if (existing)
|
|
762
763
|
return existing;
|
|
764
|
+
if (command.expectedSessionGeneration !== undefined && command.expectedSessionGeneration !== this.conversationSnapshot().sessionGeneration)
|
|
765
|
+
throw new Error('session_changed: Agent session changed. Record a new voice message.');
|
|
763
766
|
const queued = await this.queuePrompt(command.text, {
|
|
764
767
|
origin: { kind: 'owner-admin-console', commandId: command.commandId },
|
|
765
768
|
actor: { browserSession: command.actorBrowserSession },
|
|
@@ -1805,6 +1808,10 @@ export class AcpSession {
|
|
|
1805
1808
|
});
|
|
1806
1809
|
}
|
|
1807
1810
|
// ── conversation ledger access (AgentSession) ─────────────────────────────
|
|
1811
|
+
taskNoticeBinding() {
|
|
1812
|
+
return this.activeTurn && this.sessionId ? { sessionGeneration: this.sessionGeneration, acpSessionId: this.sessionId, promptId: this.activeTurn.id } : undefined;
|
|
1813
|
+
}
|
|
1814
|
+
recordTaskCreated(binding, notice) { appendTaskCreated(this.conversation, binding, notice); }
|
|
1808
1815
|
conversationPage(request = {}) {
|
|
1809
1816
|
const floor = Number(this.conversationStartCursor ?? 0);
|
|
1810
1817
|
const requested = Number(request.after ?? 0);
|
|
@@ -23,6 +23,7 @@ export declare class RoleTurnArbiter implements AgentSession {
|
|
|
23
23
|
private tail;
|
|
24
24
|
private unsettled;
|
|
25
25
|
private stopping;
|
|
26
|
+
private retiring;
|
|
26
27
|
/** Bumped by `retireStalledAdmission`; every claim carries the one it was made under. */
|
|
27
28
|
private generation;
|
|
28
29
|
/** Resolves the instant the CURRENT generation is retired. */
|
|
@@ -62,6 +63,8 @@ export declare class RoleTurnArbiter implements AgentSession {
|
|
|
62
63
|
tryScheduled(text: string, origin: Extract<PromptOrigin, {
|
|
63
64
|
kind: 'scheduled-loop';
|
|
64
65
|
}>, beforeQueue?: () => void | Promise<void>): Promise<ScheduledAttempt>;
|
|
66
|
+
/** Retire only at the same admission boundary used by all prompt producers. */
|
|
67
|
+
retireIfIdle(expired: (snapshot: SessionSnapshot, queueDepth: number) => boolean): Promise<boolean>;
|
|
65
68
|
stopScheduledAdmission(): void;
|
|
66
69
|
isAlive(): boolean;
|
|
67
70
|
snapshot(): SessionSnapshot;
|
package/dist/session/arbiter.js
CHANGED
|
@@ -29,6 +29,7 @@ export class RoleTurnArbiter {
|
|
|
29
29
|
tail = Promise.resolve();
|
|
30
30
|
unsettled = 0;
|
|
31
31
|
stopping = false;
|
|
32
|
+
retiring = false;
|
|
32
33
|
/** Bumped by `retireStalledAdmission`; every claim carries the one it was made under. */
|
|
33
34
|
generation = 0;
|
|
34
35
|
/** Resolves the instant the CURRENT generation is retired. */
|
|
@@ -89,7 +90,8 @@ export class RoleTurnArbiter {
|
|
|
89
90
|
retired.retire();
|
|
90
91
|
}
|
|
91
92
|
queuePrompt(text, options = {}) {
|
|
92
|
-
return this.exclusive(async () =>
|
|
93
|
+
return this.exclusive(async () => { if (this.retiring)
|
|
94
|
+
throw new Error('Chat is closing'); return this.track(await this.session.queuePrompt(text, options)); });
|
|
93
95
|
}
|
|
94
96
|
async submitPrompt(text, options = {}) {
|
|
95
97
|
return (await this.queuePrompt(text, options)).completion;
|
|
@@ -99,6 +101,8 @@ export class RoleTurnArbiter {
|
|
|
99
101
|
* answers and explicit interrupts must remain able to pass immediately.
|
|
100
102
|
*/
|
|
101
103
|
submitPromptAfterTool(text, options = {}) {
|
|
104
|
+
if (this.retiring)
|
|
105
|
+
return Promise.reject(new Error('Chat is closing'));
|
|
102
106
|
return this.session.submitPromptAfterTool?.(text, options)
|
|
103
107
|
?? this.session.submitPrompt(text, { ...options, interrupt: false, steer: false });
|
|
104
108
|
}
|
|
@@ -136,6 +140,19 @@ export class RoleTurnArbiter {
|
|
|
136
140
|
throw error;
|
|
137
141
|
});
|
|
138
142
|
}
|
|
143
|
+
/** Retire only at the same admission boundary used by all prompt producers. */
|
|
144
|
+
retireIfIdle(expired) {
|
|
145
|
+
return this.exclusive(async () => {
|
|
146
|
+
const snapshot = this.session.snapshot();
|
|
147
|
+
const queue = this.session.conversationSnapshot?.().queueDepth ?? -1;
|
|
148
|
+
if (this.retiring || this.unsettled > 0 || snapshot.readiness !== 'idle' || !expired(snapshot, queue))
|
|
149
|
+
return false;
|
|
150
|
+
this.retiring = true;
|
|
151
|
+
this.stopping = true;
|
|
152
|
+
await this.session.close();
|
|
153
|
+
return true;
|
|
154
|
+
});
|
|
155
|
+
}
|
|
139
156
|
stopScheduledAdmission() { this.stopping = true; }
|
|
140
157
|
isAlive() { return this.session.isAlive(); }
|
|
141
158
|
snapshot() { return this.session.snapshot(); }
|
|
@@ -157,7 +174,8 @@ export class RoleTurnArbiter {
|
|
|
157
174
|
submitPromptBrowser(command) {
|
|
158
175
|
if (!this.session.submitPromptBrowser)
|
|
159
176
|
return Promise.reject(new Error('browser prompt admission is unavailable'));
|
|
160
|
-
return this.exclusive(() => this.
|
|
177
|
+
return this.exclusive(() => { if (this.retiring)
|
|
178
|
+
throw new Error('Chat is closing'); return this.session.submitPromptBrowser(command); });
|
|
161
179
|
}
|
|
162
180
|
interrupt(source = 'local-console') {
|
|
163
181
|
return this.exclusive(async () => interruptOutcome(await this.session.interrupt(source)));
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type TaskNoticeBinding, type TaskCreatedNotice } from './task-notice.js';
|
|
1
2
|
import type { CommonPermissions } from '../config.js';
|
|
2
3
|
import type { ConversationEventV1, ConversationSnapshot, PromptOrigin, PromptReceipt, SubmitPromptCommand } from './conversation-types.js';
|
|
3
4
|
import type { CodexAppServerTransportFactory } from './codex-app-server-transport.js';
|
|
@@ -76,6 +77,8 @@ export declare class CodexAppServerSession implements AgentSession {
|
|
|
76
77
|
setControllerAttached(attached: boolean): void;
|
|
77
78
|
exitResult(): ExitRecord | null;
|
|
78
79
|
close(): Promise<void>;
|
|
80
|
+
taskNoticeBinding(): TaskNoticeBinding | undefined;
|
|
81
|
+
recordTaskCreated(binding: TaskNoticeBinding, notice: TaskCreatedNotice): void;
|
|
79
82
|
conversationPage(request?: {
|
|
80
83
|
after?: string;
|
|
81
84
|
limit?: number;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { appendTaskCreated } from './task-notice.js';
|
|
1
2
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
3
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
4
|
import { isAbsolute, join, resolve } from 'node:path';
|
|
5
|
+
import { boundedJson } from './conversation-normalizer.js';
|
|
4
6
|
import { ConversationEventStore } from './conversation-store.js';
|
|
5
7
|
import { SessionEvents } from './events.js';
|
|
6
8
|
import { CodexAppServerTransport } from './codex-app-server-transport.js';
|
|
@@ -228,8 +230,10 @@ export class CodexAppServerSession {
|
|
|
228
230
|
}
|
|
229
231
|
async interrupt(source = 'local-console') {
|
|
230
232
|
const active = this.activeTurn;
|
|
231
|
-
if (!active || !this.threadId
|
|
233
|
+
if (!active || !this.threadId)
|
|
232
234
|
return { state: 'settled' };
|
|
235
|
+
if (!active.nativeTurnId)
|
|
236
|
+
throw new SessionControlError('backend', 'The operation is still pending without a native turn ID. Cancellation is not confirmed; inspect or restart the agent if it does not progress.');
|
|
233
237
|
active.cancellationSource ??= source;
|
|
234
238
|
this.events.emit('state', { turnId: active.promptId, status: 'running', origin: active.origin });
|
|
235
239
|
this.conversation.appendSafe({
|
|
@@ -335,6 +339,10 @@ export class CodexAppServerSession {
|
|
|
335
339
|
this.conversation.close();
|
|
336
340
|
await this.transport.close();
|
|
337
341
|
}
|
|
342
|
+
taskNoticeBinding() {
|
|
343
|
+
return this.activeTurn && this.threadId ? { sessionGeneration: this.sessionGeneration, acpSessionId: this.threadId, promptId: this.activeTurn.promptId } : undefined;
|
|
344
|
+
}
|
|
345
|
+
recordTaskCreated(binding, notice) { appendTaskCreated(this.conversation, binding, notice); }
|
|
338
346
|
conversationPage(request = {}) {
|
|
339
347
|
const floor = Number(this.conversationStartCursor ?? 0);
|
|
340
348
|
const requested = Number(request.after ?? 0);
|
|
@@ -367,6 +375,8 @@ export class CodexAppServerSession {
|
|
|
367
375
|
const existing = this.conversation.receiptFor(command.commandId, bodyDigest);
|
|
368
376
|
if (existing)
|
|
369
377
|
return existing;
|
|
378
|
+
if (command.expectedSessionGeneration !== undefined && command.expectedSessionGeneration !== this.conversationSnapshot().sessionGeneration)
|
|
379
|
+
throw new Error('session_changed: Agent session changed. Record a new voice message.');
|
|
370
380
|
const queued = await this.queuePrompt(command.text, {
|
|
371
381
|
origin: { kind: 'owner-admin-console', commandId: command.commandId },
|
|
372
382
|
actor: { browserSession: command.actorBrowserSession },
|
|
@@ -415,6 +425,10 @@ export class CodexAppServerSession {
|
|
|
415
425
|
this.reasoningEffort = { value: effort };
|
|
416
426
|
this.readiness = 'idle';
|
|
417
427
|
this.events.emit('state', { status: 'idle', text: `Codex thread ${this.threadId}` });
|
|
428
|
+
this.conversation.appendSafe({
|
|
429
|
+
kind: 'capabilities.updated', sessionGeneration: this.sessionGeneration,
|
|
430
|
+
payload: { commands: [{ name: 'compact', description: textBlock('Compact session context') }] },
|
|
431
|
+
});
|
|
418
432
|
this.conversation.appendSafe({
|
|
419
433
|
kind: 'session.state', sessionGeneration: this.sessionGeneration,
|
|
420
434
|
payload: { status: 'idle' },
|
|
@@ -486,6 +500,12 @@ export class CodexAppServerSession {
|
|
|
486
500
|
promptId, turnId: promptId, source: sourceFor(origin).source, payload: {},
|
|
487
501
|
});
|
|
488
502
|
try {
|
|
503
|
+
if (text.trim() === '/compact') {
|
|
504
|
+
// The acknowledgement is not completion. Standard turn notifications
|
|
505
|
+
// settle the same queued prompt and durable browser receipt.
|
|
506
|
+
await this.transport.request('thread/compact/start', { threadId: this.threadId });
|
|
507
|
+
return await settled;
|
|
508
|
+
}
|
|
489
509
|
const response = await this.transport.request('turn/start', {
|
|
490
510
|
threadId: this.threadId,
|
|
491
511
|
input: [{ type: 'text', text, text_elements: [] }],
|
|
@@ -746,6 +766,10 @@ export class CodexAppServerSession {
|
|
|
746
766
|
toolCallId: scheduled ? 'scheduled-loop-tool' : id, snapshot,
|
|
747
767
|
title: scheduled ? 'scheduled-loop tool' : tool.title, status: tool.status,
|
|
748
768
|
kind: string(item.type),
|
|
769
|
+
...(!scheduled && item.arguments !== undefined ? { rawInput: boundedJson(item.arguments) } : {}),
|
|
770
|
+
...(!scheduled && item.type === 'commandExecution' ? { rawInput: boundedJson({ command: item.command, cwd: item.cwd }), ...(item.aggregatedOutput !== undefined ? { rawOutput: boundedJson({ output: item.aggregatedOutput, exitCode: item.exitCode }) } : {}) } : {}),
|
|
771
|
+
...(!scheduled && item.type === 'fileChange' && item.changes !== undefined ? { rawOutput: boundedJson(item.changes) } : {}),
|
|
772
|
+
...(!scheduled && (item.result !== undefined || item.error !== undefined) ? { rawOutput: boundedJson({ result: item.result, error: item.error }) } : {}),
|
|
749
773
|
},
|
|
750
774
|
});
|
|
751
775
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { LayoutControlRequest } from '../rooms-tasks/layout-control.js';
|
|
2
|
+
import type { SupervisorCorrespondence, AgentHistoryQuery } from '../agent-ours/correspondence.js';
|
|
1
3
|
import { type Socket } from 'node:net';
|
|
2
4
|
import type { AgentSession, ControlFailureKind, SessionEvent, SessionSnapshot } from './types.js';
|
|
3
5
|
import type { OwnerChannelHandle, OwnerChannelManagementRequest } from '../owner-channel/channel.js';
|
|
@@ -9,8 +11,11 @@ export interface ControlRequest {
|
|
|
9
11
|
version: 1 | 2 | 3;
|
|
10
12
|
id: string;
|
|
11
13
|
token: string;
|
|
12
|
-
command: 'status' | 'snapshot' | 'submit_prompt' | 'respond_permission' | 'interrupt' | 'follow' | 'events_since' | 'owner_channel_manage' | 'loop_status' | 'loop_run_now' | 'loop_disable' | 'loop_enable' | 'reload_config' | 'conversation_page' | 'conversation_follow' | 'submit_prompt_v2' | 'interrupt_v2' | 'respond_permission_v2' | 'fleet_spawn' | 'fleet_audit_begin' | 'fleet_audit_present' | 'fleet_audit_finish';
|
|
14
|
+
command: 'status' | 'snapshot' | 'submit_prompt' | 'respond_permission' | 'interrupt' | 'follow' | 'events_since' | 'owner_channel_manage' | 'loop_status' | 'loop_run_now' | 'loop_disable' | 'loop_enable' | 'reload_config' | 'conversation_page' | 'conversation_follow' | 'submit_prompt_v2' | 'submit_voice_prompt' | 'interrupt_v2' | 'layout_control' | 'agent_contacts' | 'agent_history' | 'respond_permission_v2' | 'fleet_spawn' | 'fleet_audit_begin' | 'fleet_audit_present' | 'fleet_audit_finish';
|
|
15
|
+
layout?: LayoutControlRequest;
|
|
16
|
+
agentHistory?: AgentHistoryQuery;
|
|
13
17
|
text?: string;
|
|
18
|
+
expectedSessionGeneration?: string;
|
|
14
19
|
permissionId?: string;
|
|
15
20
|
optionId?: string;
|
|
16
21
|
since?: number;
|
|
@@ -112,9 +117,13 @@ export declare class RoleControlServer {
|
|
|
112
117
|
private readonly sockets;
|
|
113
118
|
/** Interrupt idempotency: same command id returns the first receipt. */
|
|
114
119
|
private readonly interruptCommands;
|
|
120
|
+
private correspondence?;
|
|
121
|
+
setCorrespondence(value: SupervisorCorrespondence): void;
|
|
115
122
|
private ownerChannel?;
|
|
116
123
|
private loopManager?;
|
|
117
124
|
private reloadConfig?;
|
|
125
|
+
private layoutControl?;
|
|
126
|
+
setLayoutControl(handler: (request: LayoutControlRequest) => Promise<unknown>): void;
|
|
118
127
|
private fleetSpawner?;
|
|
119
128
|
private fleetAuditor?;
|
|
120
129
|
constructor(stateDir: string, session: AgentSession, log: (line: string) => void);
|
package/dist/session/control.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { socketPath as privateSocketPath } from '../socket-path.js';
|
|
1
2
|
import { randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
|
|
2
3
|
import { chmodSync, existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
4
|
import { createConnection, createServer } from 'node:net';
|
|
@@ -8,7 +9,7 @@ import { interruptSession, queueSessionPrompt, respondSessionPermission, respond
|
|
|
8
9
|
const MAX_LINE_BYTES = 64 * 1024;
|
|
9
10
|
/** Commands that require protocol version 3. */
|
|
10
11
|
const V3_COMMANDS = new Set([
|
|
11
|
-
'conversation_page', 'conversation_follow', 'submit_prompt_v2', 'interrupt_v2',
|
|
12
|
+
'conversation_page', 'conversation_follow', 'submit_prompt_v2', 'submit_voice_prompt', 'interrupt_v2',
|
|
12
13
|
'respond_permission_v2',
|
|
13
14
|
]);
|
|
14
15
|
/** The one retained-range projection shared by polling and live-follow admission. */
|
|
@@ -99,7 +100,7 @@ export function oversightTaxonomy(name = '<Name>') {
|
|
|
99
100
|
export function oversightTaxonomyLines(name = '<Name>') {
|
|
100
101
|
return oversightTaxonomy(name).map(r => `- **${r.result}** — ${r.meaning} → ${r.action}`);
|
|
101
102
|
}
|
|
102
|
-
export const controlSocketPath = (stateDir) => join(stateDir, '.control.sock');
|
|
103
|
+
export const controlSocketPath = (stateDir) => privateSocketPath(join(stateDir, '.control.sock'));
|
|
103
104
|
export const controlTokenPath = (stateDir) => join(stateDir, '.control-token');
|
|
104
105
|
function sameToken(actual, supplied) {
|
|
105
106
|
const a = Buffer.from(actual);
|
|
@@ -116,9 +117,13 @@ export class RoleControlServer {
|
|
|
116
117
|
sockets = new Set();
|
|
117
118
|
/** Interrupt idempotency: same command id returns the first receipt. */
|
|
118
119
|
interruptCommands = new Map();
|
|
120
|
+
correspondence;
|
|
121
|
+
setCorrespondence(value) { this.correspondence = value; }
|
|
119
122
|
ownerChannel;
|
|
120
123
|
loopManager;
|
|
121
124
|
reloadConfig;
|
|
125
|
+
layoutControl;
|
|
126
|
+
setLayoutControl(handler) { this.layoutControl = handler; }
|
|
122
127
|
fleetSpawner;
|
|
123
128
|
fleetAuditor;
|
|
124
129
|
constructor(stateDir, session, log) {
|
|
@@ -235,6 +240,7 @@ export class RoleControlServer {
|
|
|
235
240
|
protocolVersion: this.session.conversationPage ? 3 : 2,
|
|
236
241
|
features: [
|
|
237
242
|
'events_since', 'observer_follow', 'retained_range',
|
|
243
|
+
...(this.correspondence ? ['agent_correspondence'] : []),
|
|
238
244
|
...(this.session.conversationPage ? ['conversation_v3'] : []),
|
|
239
245
|
...(this.session.capabilities?.steering ? ['steering'] : []),
|
|
240
246
|
...(this.session.capabilities?.permissions ? ['permissions'] : []),
|
|
@@ -308,6 +314,13 @@ export class RoleControlServer {
|
|
|
308
314
|
});
|
|
309
315
|
return;
|
|
310
316
|
}
|
|
317
|
+
case 'layout_control': {
|
|
318
|
+
if (request.version !== 2 || !request.layout || !this.layoutControl)
|
|
319
|
+
throw new SessionControlError('rejected', 'layout control unavailable');
|
|
320
|
+
const result = await this.layoutControl(request.layout);
|
|
321
|
+
this.write(socket, { version: 1, id: request.id, ok: true, result });
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
311
324
|
case 'fleet_spawn': {
|
|
312
325
|
if (request.version !== 2 || !request.spawn || typeof request.spawn.name !== 'string')
|
|
313
326
|
throw new SessionControlError('rejected', 'version 2 and typed spawn options are required');
|
|
@@ -362,6 +375,23 @@ export class RoleControlServer {
|
|
|
362
375
|
this.write(socket, { version: 1, id: request.id, ok: true, result: { delivered: true } });
|
|
363
376
|
return;
|
|
364
377
|
}
|
|
378
|
+
case 'agent_contacts':
|
|
379
|
+
case 'agent_history': {
|
|
380
|
+
if (!this.correspondence)
|
|
381
|
+
throw new SessionControlError('rejected', 'Supervisor correspondence API is unavailable; this session needs an updated supervisor.');
|
|
382
|
+
const query = request.agentHistory;
|
|
383
|
+
if (request.command === 'agent_history' && (!query || !/^[a-f0-9]{64}$/i.test(query.peer_cid) || (query.limit !== undefined && (!Number.isInteger(query.limit) || query.limit < 1 || query.limit > 200)) || (query.before_seq !== undefined && (!Number.isSafeInteger(query.before_seq) || query.before_seq < 1))))
|
|
384
|
+
throw new SessionControlError('rejected', 'Valid peer_cid, limit (1–200) and before_seq are required.');
|
|
385
|
+
let result;
|
|
386
|
+
try {
|
|
387
|
+
result = request.command === 'agent_contacts' ? await this.correspondence.contacts() : await this.correspondence.history(query);
|
|
388
|
+
}
|
|
389
|
+
catch {
|
|
390
|
+
throw new SessionControlError('rejected', 'Supervisor correspondence read failed; identity or transport is unavailable.');
|
|
391
|
+
}
|
|
392
|
+
this.write(socket, { version: 1, id: request.id, ok: true, result });
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
365
395
|
case 'owner_channel_manage': {
|
|
366
396
|
if (!request.ownerChannel || typeof request.ownerChannel.action !== 'string')
|
|
367
397
|
throw new SessionControlError('rejected', 'owner-channel management action is required');
|
|
@@ -421,7 +451,10 @@ export class RoleControlServer {
|
|
|
421
451
|
socket.write(JSON.stringify({ version: 1, conversationEvent: event }) + '\n');
|
|
422
452
|
}) };
|
|
423
453
|
}
|
|
454
|
+
case 'submit_voice_prompt':
|
|
424
455
|
case 'submit_prompt_v2': {
|
|
456
|
+
if (request.command === 'submit_voice_prompt' && !request.expectedSessionGeneration?.trim())
|
|
457
|
+
throw new SessionControlError('rejected', 'expectedSessionGeneration is required');
|
|
425
458
|
this.requireConversation(request);
|
|
426
459
|
if (!request.commandId?.trim() || !request.text?.trim() || !request.actor?.trim()
|
|
427
460
|
|| request.source !== 'owner_admin_console')
|
|
@@ -432,6 +465,7 @@ export class RoleControlServer {
|
|
|
432
465
|
const receipt = await this.session.submitPromptBrowser({
|
|
433
466
|
commandId: request.commandId, text: request.text,
|
|
434
467
|
source: 'owner_admin_console', actorBrowserSession: request.actor,
|
|
468
|
+
expectedSessionGeneration: request.expectedSessionGeneration,
|
|
435
469
|
});
|
|
436
470
|
this.write(socket, { version: 1, id: request.id, ok: true, result: receipt });
|
|
437
471
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type * as acp from '@agentclientprotocol/sdk';
|
|
2
|
-
import type { AdapterMeta, ConversationEventKind, ConversationPayload } from './conversation-types.js';
|
|
2
|
+
import type { AdapterMeta, BoundedJson, ConversationEventKind, ConversationPayload } from './conversation-types.js';
|
|
3
3
|
/**
|
|
4
4
|
* Reduce one ACP v1 `session/update` into one conversation-domain event draft.
|
|
5
5
|
*
|
|
@@ -43,4 +43,5 @@ export interface NormalizedUpdate {
|
|
|
43
43
|
toolCallId?: string;
|
|
44
44
|
adapterMeta?: AdapterMeta[];
|
|
45
45
|
}
|
|
46
|
+
export declare function boundedJson(value: unknown): BoundedJson;
|
|
46
47
|
export declare function normalizeSessionUpdate(update: acp.SessionUpdate, options?: NormalizeOptions): NormalizedUpdate;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { TaskCreatedNotice } from './task-notice.js';
|
|
1
2
|
import type { PromptOrigin, TurnCancellationSource, TurnOutcome } from './types.js';
|
|
2
3
|
/**
|
|
3
4
|
* Durable conversation domain schema for managed-session consoles.
|
|
@@ -7,11 +8,11 @@ import type { PromptOrigin, TurnCancellationSource, TurnOutcome } from './types.
|
|
|
7
8
|
* file touches the wire: transport updates are reduced into these shapes by a
|
|
8
9
|
* session adapter, and the store assigns `seq`/`eventId`/timestamps.
|
|
9
10
|
*/
|
|
10
|
-
export type ConversationEventKind = 'prompt.admitted' | 'prompt.started' | 'prompt.interrupt_requested' | 'message.chunk' | 'message.replace' | 'thought.chunk' | 'thought.replace' | 'plan.replace' | 'tool.upsert' | 'tool.content_chunk' | 'permission.requested' | 'permission.resolved' | 'monitor.delivery' | 'usage.updated' | 'turn.state' | 'turn.completed' | 'session.state' | 'session.info' | 'capabilities.updated' | 'error'
|
|
11
|
+
export type ConversationEventKind = 'prompt.admitted' | 'prompt.started' | 'prompt.interrupt_requested' | 'message.chunk' | 'message.replace' | 'thought.chunk' | 'thought.replace' | 'plan.replace' | 'tool.upsert' | 'tool.content_chunk' | 'permission.requested' | 'permission.resolved' | 'fleet.task_created' | 'monitor.delivery' | 'usage.updated' | 'turn.state' | 'turn.completed' | 'session.state' | 'session.info' | 'capabilities.updated' | 'error'
|
|
11
12
|
/** A well-formed transport update this version cannot represent. Bounded, never a crash. */
|
|
12
13
|
| 'unsupported';
|
|
13
14
|
/** Where a conversation record came from. Typed provenance, never prompt text. */
|
|
14
|
-
export type ConversationSource = 'owner_admin_console' | 'owner_channel' | 'fleet_monitor' | 'scheduled_loop' | 'startup' | 'local_console' | 'agent' | 'agent_replay';
|
|
15
|
+
export type ConversationSource = 'owner_admin_console' | 'owner_channel' | 'fleet_monitor' | 'scheduled_loop' | 'startup' | 'local_console' | 'agent' | 'agent_replay' | 'fleet_lifecycle';
|
|
15
16
|
export interface NormalizedText {
|
|
16
17
|
type: 'text';
|
|
17
18
|
text: string;
|
|
@@ -227,7 +228,7 @@ export interface BoundedJson {
|
|
|
227
228
|
digest?: string;
|
|
228
229
|
redacted?: true;
|
|
229
230
|
}
|
|
230
|
-
export type ConversationPayload = MessageChunkPayload | ThoughtChunkPayload | PlanReplacePayload | ToolUpsertPayload | UsageUpdatedPayload | SessionStatePayload | SessionInfoPayload | CapabilitiesUpdatedPayload | UnsupportedPayload | PromptAdmittedPayload | PromptStartedPayload | PromptInterruptRequestedPayload | PermissionRequestedPayload | PermissionResolvedPayload | TurnStatePayload | TurnCompletedPayload | MonitorDeliveryPayload | SessionLifecyclePayload | ErrorPayload;
|
|
231
|
+
export type ConversationPayload = MessageChunkPayload | ThoughtChunkPayload | PlanReplacePayload | ToolUpsertPayload | UsageUpdatedPayload | SessionStatePayload | SessionInfoPayload | CapabilitiesUpdatedPayload | UnsupportedPayload | PromptAdmittedPayload | PromptStartedPayload | PromptInterruptRequestedPayload | PermissionRequestedPayload | PermissionResolvedPayload | TurnStatePayload | TurnCompletedPayload | MonitorDeliveryPayload | SessionLifecyclePayload | ErrorPayload | TaskCreatedNotice;
|
|
231
232
|
export interface ConversationEventV1 {
|
|
232
233
|
schemaVersion: 1;
|
|
233
234
|
roleId: string;
|
|
@@ -262,6 +263,7 @@ export interface ConversationEventV1 {
|
|
|
262
263
|
adapterMeta?: AdapterMeta[];
|
|
263
264
|
}
|
|
264
265
|
export interface SubmitPromptCommand {
|
|
266
|
+
expectedSessionGeneration?: string;
|
|
265
267
|
/** Idempotency-Key / clientRequestId. Reuse with a different body is a conflict. */
|
|
266
268
|
commandId: string;
|
|
267
269
|
text: string;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ConversationEventStore } from './conversation-store.js';
|
|
2
|
+
export interface TaskNoticeBinding {
|
|
3
|
+
sessionGeneration: string;
|
|
4
|
+
acpSessionId: string;
|
|
5
|
+
promptId: string;
|
|
6
|
+
}
|
|
7
|
+
export interface TaskCreatedNotice {
|
|
8
|
+
operationId: string;
|
|
9
|
+
taskId: string;
|
|
10
|
+
title: string;
|
|
11
|
+
state: string;
|
|
12
|
+
}
|
|
13
|
+
/** Only the trusted Fleet audit completion calls this, never model output. */
|
|
14
|
+
export declare function appendTaskCreated(store: ConversationEventStore, binding: TaskNoticeBinding, notice: TaskCreatedNotice): void;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Only the trusted Fleet audit completion calls this, never model output. */
|
|
2
|
+
export function appendTaskCreated(store, binding, notice) {
|
|
3
|
+
let after;
|
|
4
|
+
do {
|
|
5
|
+
const page = store.page({ after, limit: 1000 });
|
|
6
|
+
if (page.events.some(e => e.kind === 'fleet.task_created' && e.payload.operationId === notice.operationId))
|
|
7
|
+
return;
|
|
8
|
+
if (!page.hasMore)
|
|
9
|
+
break;
|
|
10
|
+
after = page.nextCursor;
|
|
11
|
+
} while (after);
|
|
12
|
+
store.append({ kind: 'fleet.task_created', source: 'fleet_lifecycle', ...binding, turnId: binding.promptId, payload: notice });
|
|
13
|
+
}
|
package/dist/session/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { TaskCreatedNotice, TaskNoticeBinding } from './task-notice.js';
|
|
1
2
|
import type { SessionBackendId } from '../config.js';
|
|
2
3
|
import type { ConversationEventV1, ConversationSnapshot, PromptReceipt, SubmitPromptCommand } from './conversation-types.js';
|
|
3
4
|
/**
|
|
@@ -289,6 +290,8 @@ export interface AgentSession {
|
|
|
289
290
|
readonly capabilities?: AgentSessionCapabilities;
|
|
290
291
|
isAlive(): boolean;
|
|
291
292
|
snapshot(): SessionSnapshot;
|
|
293
|
+
taskNoticeBinding?(): TaskNoticeBinding | undefined;
|
|
294
|
+
recordTaskCreated?(binding: TaskNoticeBinding, notice: TaskCreatedNotice): void;
|
|
292
295
|
conversationPage?(request: {
|
|
293
296
|
after?: string;
|
|
294
297
|
limit?: number;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { lstatSync, mkdirSync } from 'node:fs';
|
|
3
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
4
|
+
/** Optional private socket directory for deeply nested, isolated Fleet homes. */
|
|
5
|
+
export function socketPath(logicalPath) {
|
|
6
|
+
const root = process.env.OURS_FLEET_SOCKET_ROOT;
|
|
7
|
+
if (!root) {
|
|
8
|
+
if (Buffer.byteLength(logicalPath) > 103)
|
|
9
|
+
throw new Error('Unix socket path is too long; configure a short private OURS_FLEET_SOCKET_ROOT');
|
|
10
|
+
return logicalPath;
|
|
11
|
+
}
|
|
12
|
+
if (!isAbsolute(root) || resolve(root) !== root)
|
|
13
|
+
throw new Error('OURS_FLEET_SOCKET_ROOT must be an absolute normalized path');
|
|
14
|
+
mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
15
|
+
const stat = lstatSync(root);
|
|
16
|
+
if (!stat.isDirectory() || stat.isSymbolicLink() || stat.uid !== process.getuid?.() || (stat.mode & 0o777) !== 0o700)
|
|
17
|
+
throw new Error('OURS_FLEET_SOCKET_ROOT must be an owner-only 0700 directory');
|
|
18
|
+
const path = join(root, createHash('sha256').update(resolve(logicalPath)).digest('hex').slice(0, 24) + '.sock');
|
|
19
|
+
if (Buffer.byteLength(path) > 103)
|
|
20
|
+
throw new Error('OURS_FLEET_SOCKET_ROOT is too long');
|
|
21
|
+
return path;
|
|
22
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { SessionSnapshot } from './session/types.js';
|
|
2
|
+
export declare const TEMP_CHAT_IDLE_MS: number;
|
|
3
|
+
export type ChatIdlePolicy = {
|
|
4
|
+
version: 1;
|
|
5
|
+
timeoutMs: number;
|
|
6
|
+
lastActivityAt: number;
|
|
7
|
+
};
|
|
8
|
+
export declare function readChatIdle(dir: string): ChatIdlePolicy | undefined;
|
|
9
|
+
/** Only a standalone temporary supervisor owns this clock; browser reads never touch it. */
|
|
10
|
+
export declare class TemporaryChatIdle {
|
|
11
|
+
private dir;
|
|
12
|
+
private value;
|
|
13
|
+
private busy;
|
|
14
|
+
constructor(dir: string, now: number);
|
|
15
|
+
private save;
|
|
16
|
+
observe(snapshot: SessionSnapshot, queueDepth: number, now: number): boolean;
|
|
17
|
+
}
|