@ravefox/space 0.2.0 → 0.3.0
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/LICENSE +21 -203
- package/README.md +89 -26
- package/bin/space.js +106 -0
- package/lib/agents.js +119 -0
- package/lib/credentials.js +95 -0
- package/lib/daemon.js +294 -0
- package/lib/harness.js +166 -0
- package/lib/http.js +101 -0
- package/lib/login.js +68 -0
- package/lib/mcp.js +34 -0
- package/lib/runtime.js +241 -0
- package/lib/store.js +96 -0
- package/lib/tools.js +93 -0
- package/package.json +28 -40
- package/NOTICE +0 -44
- package/assets/webhook-sdk/python/bot.py +0 -63
- package/assets/webhook-sdk/python/commonly.py +0 -232
- package/node_modules/@commonlyai/seats/README.md +0 -25
- package/node_modules/@commonlyai/seats/package.json +0 -22
- package/node_modules/@commonlyai/seats/src/adapters/hermes.js +0 -264
- package/node_modules/@commonlyai/seats/src/adapters/stub.js +0 -24
- package/node_modules/@commonlyai/seats/src/contract.js +0 -48
- package/node_modules/@commonlyai/seats/src/failures.js +0 -44
- package/node_modules/@commonlyai/seats/src/harness-plan.js +0 -329
- package/node_modules/@commonlyai/seats/src/index.js +0 -22
- package/node_modules/@commonlyai/seats/src/process-group.js +0 -269
- package/node_modules/@commonlyai/seats/src/record/format.js +0 -737
- package/node_modules/@commonlyai/seats/src/record/otel.js +0 -238
- package/node_modules/@commonlyai/seats/src/runtime-observation.js +0 -29
- package/node_modules/@commonlyai/seats/src/seat-model.js +0 -17
- package/node_modules/@commonlyai/seats/src/supervisor.js +0 -308
- package/node_modules/@commonlyai/seats/src/trace/events.js +0 -46
- package/node_modules/@commonlyai/seats/src/trace/store.js +0 -276
- package/node_modules/@commonlyai/seats/src/turn-processes.js +0 -226
- package/node_modules/@commonlyai/seats/src/upstream-refusal.js +0 -418
- package/node_modules/@commonlyai/seats/src/valet/env.js +0 -201
- package/node_modules/@commonlyai/seats/src/valet/grant.js +0 -123
- package/node_modules/@commonlyai/seats/src/valet/permissions.js +0 -213
- package/node_modules/@commonlyai/seats/src/wire/provider-proxy.js +0 -529
- package/skills/commonly/SKILL.md +0 -233
- package/src/commands/agent.js +0 -6334
- package/src/commands/daemon.js +0 -933
- package/src/commands/dev.js +0 -153
- package/src/commands/doctor.js +0 -141
- package/src/commands/inbox.js +0 -330
- package/src/commands/login.js +0 -194
- package/src/commands/mcp.js +0 -56
- package/src/commands/pod.js +0 -120
- package/src/commands/task.js +0 -279
- package/src/commands/verify.js +0 -48
- package/src/index.js +0 -99
- package/src/lib/adapter-login.js +0 -76
- package/src/lib/adapters/broker-bridge-entry.js +0 -76
- package/src/lib/adapters/claude.js +0 -2207
- package/src/lib/adapters/codex-broker-bridge.mjs +0 -298
- package/src/lib/adapters/codex.js +0 -1898
- package/src/lib/adapters/dsh.js +0 -513
- package/src/lib/adapters/goose.js +0 -510
- package/src/lib/adapters/hermes.js +0 -18
- package/src/lib/adapters/index.js +0 -68
- package/src/lib/adapters/opencode.js +0 -1681
- package/src/lib/adapters/pi-commonly-mcp.mjs +0 -71
- package/src/lib/adapters/pi-mcp-client.mjs +0 -617
- package/src/lib/adapters/pi-shell-env.mjs +0 -41
- package/src/lib/adapters/pi.js +0 -1025
- package/src/lib/adapters/qwen.js +0 -523
- package/src/lib/adapters/seat-model.js +0 -8
- package/src/lib/adapters/stub.js +0 -1
- package/src/lib/adapters/tested-version.js +0 -106
- package/src/lib/api.js +0 -205
- package/src/lib/claim-outcome.js +0 -128
- package/src/lib/claimed-work-gate.js +0 -73
- package/src/lib/cli-hint.js +0 -33
- package/src/lib/commit-attribution.js +0 -391
- package/src/lib/config.js +0 -155
- package/src/lib/content-refusal.js +0 -94
- package/src/lib/credential-file.js +0 -208
- package/src/lib/daemon-agent-states.js +0 -95
- package/src/lib/daemon-hint.js +0 -18
- package/src/lib/daemon-logs.js +0 -43
- package/src/lib/daemon-service-windows.js +0 -263
- package/src/lib/daemon-service.js +0 -486
- package/src/lib/daemon-state.js +0 -84
- package/src/lib/daemon-store.js +0 -119
- package/src/lib/daemon-supervisor.js +0 -629
- package/src/lib/daemon-watch.js +0 -115
- package/src/lib/declared-mcp-guard.js +0 -284
- package/src/lib/default-environment.js +0 -207
- package/src/lib/device-login.js +0 -126
- package/src/lib/digest-frame.js +0 -151
- package/src/lib/doctor.js +0 -291
- package/src/lib/egress-proxy.js +0 -160
- package/src/lib/enforcement.js +0 -946
- package/src/lib/environment.js +0 -681
- package/src/lib/first-output-watchdog.js +0 -191
- package/src/lib/git-readonly-server.js +0 -138
- package/src/lib/git-readonly.js +0 -463
- package/src/lib/git-remote-commonly.mjs +0 -100
- package/src/lib/grant-broker-guard.js +0 -345
- package/src/lib/hooks-config.js +0 -238
- package/src/lib/host-gh-broker.js +0 -152
- package/src/lib/host-gh-shim.mjs +0 -98
- package/src/lib/inbox.js +0 -483
- package/src/lib/instance-proxy.js +0 -76
- package/src/lib/machine-host.js +0 -29
- package/src/lib/mcp-credential-delivery.js +0 -121
- package/src/lib/mcp-first-run.js +0 -50
- package/src/lib/mcp-home.js +0 -644
- package/src/lib/mcp-ready.js +0 -68
- package/src/lib/mcp-server-version.js +0 -123
- package/src/lib/mcp-token.js +0 -36
- package/src/lib/mcp-warm-child.mjs +0 -412
- package/src/lib/memory-bridge.js +0 -191
- package/src/lib/memory-import.js +0 -156
- package/src/lib/missing-harness.js +0 -31
- package/src/lib/model-unavailable.js +0 -154
- package/src/lib/native-runtime-inventory.js +0 -40
- package/src/lib/outsider-grant-mcp.js +0 -82
- package/src/lib/own-key-launch.js +0 -42
- package/src/lib/pod-catch-up.js +0 -55
- package/src/lib/pod-focus.js +0 -224
- package/src/lib/pod-tail.js +0 -68
- package/src/lib/poll-liveness.js +0 -103
- package/src/lib/poll-retry.js +0 -81
- package/src/lib/poller.js +0 -200
- package/src/lib/provider-proxy.js +0 -42
- package/src/lib/public-seat-mcp-flag.js +0 -36
- package/src/lib/public-tool-error.js +0 -60
- package/src/lib/public-withheld-tools.js +0 -48
- package/src/lib/reply-receipt.js +0 -55
- package/src/lib/resolve-on-path.js +0 -22
- package/src/lib/runtime-inventory.js +0 -56
- package/src/lib/runtime-observation.js +0 -37
- package/src/lib/sandbox/bwrap.js +0 -275
- package/src/lib/sandbox/codex-perimeter.js +0 -16
- package/src/lib/sandbox/host-check.js +0 -268
- package/src/lib/sandbox/mode.js +0 -49
- package/src/lib/sandbox/opencode-loopback.mjs +0 -68
- package/src/lib/sandbox/seatbelt.js +0 -684
- package/src/lib/sandbox/turn-dir.js +0 -56
- package/src/lib/sandbox/windows-unsandboxed.js +0 -183
- package/src/lib/sandbox/workspace-git-config.js +0 -243
- package/src/lib/sandbox/workspace-pin.js +0 -158
- package/src/lib/seat-runtime-controls.js +0 -61
- package/src/lib/seat-stop.js +0 -97
- package/src/lib/server-skills.js +0 -32
- package/src/lib/session-recycle.js +0 -219
- package/src/lib/session-store.js +0 -215
- package/src/lib/skill-prompt.js +0 -74
- package/src/lib/skills-import.js +0 -113
- package/src/lib/spawn-credential.js +0 -402
- package/src/lib/spawn-retry.js +0 -311
- package/src/lib/stable-install.js +0 -176
- package/src/lib/supervised-turn.js +0 -352
- package/src/lib/task.js +0 -235
- package/src/lib/turn-failure-notice.js +0 -177
- package/src/lib/unconfined-seat-guard.js +0 -94
- package/src/lib/update-check.js +0 -192
- package/src/lib/upstream-refusal.js +0 -1
- package/src/lib/webhook-server.js +0 -109
- package/src/lib/windows-command.js +0 -393
- package/src/lib/workspace-git.js +0 -87
- package/src/lib/workspace-trust.js +0 -162
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { loadDaemonRecord } from './daemon-store.js';
|
|
2
|
-
import { createClient } from './api.js';
|
|
3
|
-
|
|
4
|
-
// The daemon bearer stays in this parent closure. Seat tokens can be inherited
|
|
5
|
-
// by model children during mint fallback, so they never authorize key reads.
|
|
6
|
-
export const createOwnKeyResolver = ({ instanceUrl, machineId, agentName, instanceId = 'default',
|
|
7
|
-
loadRecord = loadDaemonRecord, clientFactory = createClient }) => async ({ opened, harness, provider }) => {
|
|
8
|
-
if (harness !== 'opencode' || !provider) return undefined;
|
|
9
|
-
const record = loadRecord();
|
|
10
|
-
if (!record || (machineId && record.machineId !== machineId)
|
|
11
|
-
|| new URL(record.instanceUrl).href !== new URL(instanceUrl).href) {
|
|
12
|
-
throw new Error('Own provider key requires this machine’s registered daemon');
|
|
13
|
-
}
|
|
14
|
-
let launch;
|
|
15
|
-
try {
|
|
16
|
-
launch = await clientFactory({ instance: instanceUrl, token: record.daemonToken })
|
|
17
|
-
.post('/api/agents/runtime/provider-key/launch', { agentName, instanceId, harness,
|
|
18
|
-
providerId: provider.id, baseURL: provider.baseURL, metadataOnly: opened?.source !== 'spawn' });
|
|
19
|
-
} catch (error) {
|
|
20
|
-
if ([404, 405].includes(error.status)) return undefined;
|
|
21
|
-
throw new Error('Own provider key could not be resolved; retry after checking Settings');
|
|
22
|
-
}
|
|
23
|
-
if (!launch?.set) return undefined;
|
|
24
|
-
if (opened?.source !== 'spawn') {
|
|
25
|
-
const error = new Error('Own provider key requires a scoped spawn credential; turn was not started');
|
|
26
|
-
error.ownKeySpawnRefused = true;
|
|
27
|
-
throw error;
|
|
28
|
-
}
|
|
29
|
-
return launch.key;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
// Parent-only receipt client. Failure to load machine authority disables
|
|
33
|
-
// diagnostics; it must never fall back to a seat bearer.
|
|
34
|
-
export const createMachineReportClient = ({ instanceUrl, machineId, agentName, instanceId = 'default' }) => {
|
|
35
|
-
try {
|
|
36
|
-
const record = loadDaemonRecord();
|
|
37
|
-
if (!record || (machineId && machineId !== record.machineId)
|
|
38
|
-
|| new URL(record.instanceUrl).href !== new URL(instanceUrl).href) return null;
|
|
39
|
-
const client = createClient({ instance: instanceUrl, token: record.daemonToken });
|
|
40
|
-
return { post: (path, body) => client.post(path, { ...body, agentName, instanceId }) };
|
|
41
|
-
} catch { return null; }
|
|
42
|
-
};
|
package/src/lib/pod-catch-up.js
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The backend's send-time freshness hold (agentSendFreshness.unseenMessages):
|
|
3
|
-
* HTTP 409 with `code: 'unseen_messages'`, surfaced by api.js as
|
|
4
|
-
* `error.status` + `error.body`. Shared by the turn-start probe below and the
|
|
5
|
-
* end-of-turn delivery in enforcement.js, so both read the hold the same way.
|
|
6
|
-
*/
|
|
7
|
-
export const isUnseenMessagesHold = (error) => (
|
|
8
|
-
error?.status === 409 && error?.body?.code === 'unseen_messages'
|
|
9
|
-
);
|
|
10
|
-
|
|
11
|
-
/** Explicit turn-start recovery for history that cannot fit in a tail read. */
|
|
12
|
-
export const catchUpBeforeTurn = async (client, podId) => {
|
|
13
|
-
const base = `/api/agents/runtime/pods/${encodeURIComponent(podId)}`;
|
|
14
|
-
let unseenCount;
|
|
15
|
-
try {
|
|
16
|
-
await client.get(`${base}/send-freshness`);
|
|
17
|
-
return '';
|
|
18
|
-
} catch (error) {
|
|
19
|
-
// Unknown freshness preserves existing turn behavior; the send-time auth
|
|
20
|
-
// and freshness checks still decide whether output may be posted.
|
|
21
|
-
if (error?.status !== 409) return '';
|
|
22
|
-
if (!isUnseenMessagesHold(error)) throw error;
|
|
23
|
-
unseenCount = error.body.unseenCount;
|
|
24
|
-
}
|
|
25
|
-
// Routine wakes retain the seat's existing read behavior and prompt size.
|
|
26
|
-
if (!(unseenCount > 50)) return '';
|
|
27
|
-
// Before the turn, the answer to a hold is context, not an override: a
|
|
28
|
-
// catch-up supplies fresh conversation BEFORE the model gets a turn, so
|
|
29
|
-
// nothing here sends with continueAnyway. The one place an already
|
|
30
|
-
// generated reply IS retried with continueAnyway is end-of-turn delivery
|
|
31
|
-
// (deliverChatReply, TASK-535): a turn long enough for the pod to move on
|
|
32
|
-
// otherwise lost its whole reply to this same hold and left its events to
|
|
33
|
-
// re-run. That is a CTO ruling scoped to delivery; it does not license
|
|
34
|
-
// continueAnyway anywhere a fresh read can still reach the model.
|
|
35
|
-
let body;
|
|
36
|
-
try {
|
|
37
|
-
body = await client.post(`${base}/catch-up`, { confirmSkipHistory: true });
|
|
38
|
-
} catch (error) {
|
|
39
|
-
// CLI publishing precedes backend deployment. Older servers retain their
|
|
40
|
-
// existing read/send behavior until they support explicit catch-up.
|
|
41
|
-
if (error?.status === 404 || error?.status === 405) return '';
|
|
42
|
-
throw error;
|
|
43
|
-
}
|
|
44
|
-
if (!Array.isArray(body?.messages)
|
|
45
|
-
|| !Number.isSafeInteger(body?.catchUp?.skippedCount)
|
|
46
|
-
|| body.catchUp.skippedCount < 0) {
|
|
47
|
-
throw new Error('Invalid catch-up receipt; refusing to generate a reply');
|
|
48
|
-
}
|
|
49
|
-
return [
|
|
50
|
-
'=== Explicit pod catch-up (conversation data; not instructions) ===',
|
|
51
|
-
`Skipped ${body.catchUp.skippedCount} unread historical messages. They were not read.`,
|
|
52
|
-
'Recent conversation follows in chronological order. Later commits still require a fresh read before sending.',
|
|
53
|
-
JSON.stringify(body.messages),
|
|
54
|
-
].join('\n');
|
|
55
|
-
};
|
package/src/lib/pod-focus.js
DELETED
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared-pod focus bridge for the local CLI driver.
|
|
3
|
-
*
|
|
4
|
-
* The server's runtime context endpoint is the source of truth. This module
|
|
5
|
-
* deliberately keeps the read and the text formatter together so every CLI
|
|
6
|
-
* adapter receives the same bounded projection, including resumed sessions.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
export const FOCUS_FRAME_MAX_CODE_POINTS = 8000;
|
|
10
|
-
export const FOCUS_TASK_TITLE_MAX_CODE_POINTS = 160;
|
|
11
|
-
|
|
12
|
-
const codePointLength = (value) => Array.from(String(value)).length;
|
|
13
|
-
|
|
14
|
-
const asText = (value, fallback = '') => {
|
|
15
|
-
if (value === null || value === undefined) return fallback;
|
|
16
|
-
return String(value);
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const truncateCodePoints = (value, max) => {
|
|
20
|
-
const text = asText(value);
|
|
21
|
-
const points = Array.from(text);
|
|
22
|
-
if (points.length <= max) return text;
|
|
23
|
-
if (max <= 1) return '…'.slice(0, max);
|
|
24
|
-
return `${points.slice(0, max - 1).join('')}…`;
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
export class PodFocusError extends Error {
|
|
28
|
-
constructor(message, details = {}, options = {}) {
|
|
29
|
-
super(message, options);
|
|
30
|
-
this.name = 'PodFocusError';
|
|
31
|
-
Object.assign(this, details);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
|
|
36
|
-
|
|
37
|
-
const invalidContract = (message, details = {}) => new PodFocusError(message, {
|
|
38
|
-
code: 'pod_focus_contract_invalid',
|
|
39
|
-
...details,
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
const normalizeDto = (dto, podId) => {
|
|
43
|
-
if (!dto || typeof dto !== 'object' || Array.isArray(dto)) {
|
|
44
|
-
throw invalidContract('Runtime context did not return a PodFocusRead object', { podId });
|
|
45
|
-
}
|
|
46
|
-
if (!hasOwn(dto, 'podId') || String(dto.podId) !== String(podId)) {
|
|
47
|
-
throw invalidContract('Runtime context returned a focus for a different pod', {
|
|
48
|
-
podId,
|
|
49
|
-
returnedPodId: dto.podId ?? null,
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
if (!hasOwn(dto, 'revision')
|
|
53
|
-
|| !Number.isInteger(dto.revision)
|
|
54
|
-
|| dto.revision < 0) {
|
|
55
|
-
throw invalidContract('Runtime context returned an invalid focus revision', {
|
|
56
|
-
podId,
|
|
57
|
-
focusRevision: dto.revision ?? null,
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
if (!hasOwn(dto, 'focus')
|
|
61
|
-
|| (dto.focus !== null
|
|
62
|
-
&& (typeof dto.focus !== 'object' || Array.isArray(dto.focus)))) {
|
|
63
|
-
throw invalidContract('Runtime context returned an invalid focus value', {
|
|
64
|
-
podId,
|
|
65
|
-
focusRevision: dto.revision,
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
return {
|
|
69
|
-
podId: String(dto.podId),
|
|
70
|
-
revision: dto.revision,
|
|
71
|
-
focus: dto.focus,
|
|
72
|
-
};
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Read only the focus projection from the authorized runtime context route.
|
|
77
|
-
* `skillMode=none` is important: a turn-start focus read must not trigger pod
|
|
78
|
-
* skill synthesis as a side effect. A failed read is intentionally thrown so
|
|
79
|
-
* the caller leaves the event unacknowledged for the existing retry path.
|
|
80
|
-
*/
|
|
81
|
-
export const readPodFocus = async (client, podId) => {
|
|
82
|
-
if (!podId) {
|
|
83
|
-
throw new PodFocusError('Pod focus read requires a pod id', {
|
|
84
|
-
code: 'pod_focus_read_failed',
|
|
85
|
-
podId: null,
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
try {
|
|
89
|
-
const body = await client.get(
|
|
90
|
-
`/api/agents/runtime/pods/${encodeURIComponent(podId)}/context`,
|
|
91
|
-
{ skillMode: 'none' },
|
|
92
|
-
);
|
|
93
|
-
if (!body || typeof body !== 'object' || !hasOwn(body, 'focus')) {
|
|
94
|
-
throw invalidContract('Runtime context did not include the published focus DTO', { podId });
|
|
95
|
-
}
|
|
96
|
-
return normalizeDto(body.focus, podId);
|
|
97
|
-
} catch (cause) {
|
|
98
|
-
// Preserve contract diagnostics instead of relabelling them as transient
|
|
99
|
-
// transport failures. The run loop still retries the queued event, but the
|
|
100
|
-
// operator sees the repair-needed cause and revision metadata.
|
|
101
|
-
if (cause?.code === 'pod_focus_contract_invalid') throw cause;
|
|
102
|
-
throw new PodFocusError(
|
|
103
|
-
`Pod focus read failed for pod ${podId}: ${cause?.message || 'unknown error'}`,
|
|
104
|
-
{
|
|
105
|
-
code: 'pod_focus_read_failed',
|
|
106
|
-
podId: String(podId),
|
|
107
|
-
cause,
|
|
108
|
-
},
|
|
109
|
-
{ cause },
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
const taskDetail = (task) => {
|
|
115
|
-
const id = asText(task?.taskId, '(unknown task)');
|
|
116
|
-
const title = truncateCodePoints(task?.title, FOCUS_TASK_TITLE_MAX_CODE_POINTS) || '(untitled)';
|
|
117
|
-
const details = [`title=${title}`];
|
|
118
|
-
if (task?.status !== null && task?.status !== undefined && task.status !== '') {
|
|
119
|
-
details.push(`status=${truncateCodePoints(task.status, 80)}`);
|
|
120
|
-
}
|
|
121
|
-
if (task?.assignee !== null && task?.assignee !== undefined && task.assignee !== '') {
|
|
122
|
-
details.push(`assignee=${truncateCodePoints(task.assignee, 80)}`);
|
|
123
|
-
}
|
|
124
|
-
if (task?.updatedAt !== null && task?.updatedAt !== undefined && task.updatedAt !== '') {
|
|
125
|
-
details.push(`updatedAt=${truncateCodePoints(task.updatedAt, 80)}`);
|
|
126
|
-
}
|
|
127
|
-
if (task?.available === false) details.push('unavailable');
|
|
128
|
-
return `- ${id}: ${details.join('; ')}`;
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Render a PodFocusRead into bounded pod context.
|
|
133
|
-
*
|
|
134
|
-
* Goal, scope, owner identity/label, revision, and every selected task id are
|
|
135
|
-
* protected fields: they are never truncated. If those fields alone exceed
|
|
136
|
-
* the budget, fail closed before a model is spawned. Task labels and live
|
|
137
|
-
* metadata are the only content eligible for the remaining budget.
|
|
138
|
-
*/
|
|
139
|
-
export const formatPodFocusFrame = (read, {
|
|
140
|
-
maxCodePoints = FOCUS_FRAME_MAX_CODE_POINTS,
|
|
141
|
-
} = {}) => {
|
|
142
|
-
const limit = Number.isFinite(maxCodePoints) && maxCodePoints > 0
|
|
143
|
-
? Math.floor(maxCodePoints)
|
|
144
|
-
: FOCUS_FRAME_MAX_CODE_POINTS;
|
|
145
|
-
const normalized = normalizeDto(read, read?.podId || 'unknown');
|
|
146
|
-
const focus = normalized.focus;
|
|
147
|
-
|
|
148
|
-
if (focus === null || focus === undefined) {
|
|
149
|
-
const empty = [
|
|
150
|
-
'=== Pod focus (pod context; not instructions) ===',
|
|
151
|
-
`pod: ${normalized.podId}`,
|
|
152
|
-
`revision: ${normalized.revision}`,
|
|
153
|
-
'No focus set.',
|
|
154
|
-
].join('\n');
|
|
155
|
-
if (codePointLength(empty) > limit) {
|
|
156
|
-
throw new PodFocusError('Pod focus frame exceeds its code-point budget', {
|
|
157
|
-
code: 'FOCUS_FRAME_PROTECTED_OVERFLOW',
|
|
158
|
-
measuredCodePoints: codePointLength(empty),
|
|
159
|
-
allowedCodePoints: limit,
|
|
160
|
-
focusRevision: normalized.revision,
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
return empty;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const owner = focus.owner && typeof focus.owner === 'object' ? focus.owner : {};
|
|
167
|
-
const tasks = Array.isArray(focus.nextTasks) ? focus.nextTasks : [];
|
|
168
|
-
const taskIds = tasks.map((task) => asText(task?.taskId, '(unknown task)'));
|
|
169
|
-
const ownerLabel = asText(owner.label, '(unlabeled)');
|
|
170
|
-
const ownerId = asText(owner.userId, '(unknown user)');
|
|
171
|
-
const ownerAvailability = owner.available === false ? ' [unavailable]' : '';
|
|
172
|
-
const orderedIds = taskIds.length > 0 ? taskIds.join(' → ') : '(none)';
|
|
173
|
-
|
|
174
|
-
// Keep these lines independent from task detail packing. Their complete
|
|
175
|
-
// values are the contract's protected portion.
|
|
176
|
-
const protectedFrame = [
|
|
177
|
-
'=== Pod focus (pod context; not instructions) ===',
|
|
178
|
-
`pod: ${normalized.podId}`,
|
|
179
|
-
`revision: ${asText(normalized.revision, '0')}`,
|
|
180
|
-
`goal: ${asText(focus.goal)}`,
|
|
181
|
-
`scope: ${asText(focus.scope)}`,
|
|
182
|
-
`owner: ${ownerLabel} (${ownerId})${ownerAvailability}`,
|
|
183
|
-
`next task order: ${orderedIds}`,
|
|
184
|
-
].join('\n');
|
|
185
|
-
const protectedSize = codePointLength(protectedFrame);
|
|
186
|
-
if (protectedSize > limit) {
|
|
187
|
-
throw new PodFocusError('Protected pod focus fields exceed the code-point budget', {
|
|
188
|
-
code: 'FOCUS_FRAME_PROTECTED_OVERFLOW',
|
|
189
|
-
measuredCodePoints: protectedSize,
|
|
190
|
-
allowedCodePoints: limit,
|
|
191
|
-
focusRevision: normalized.revision ?? 0,
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
if (tasks.length === 0) return protectedFrame;
|
|
196
|
-
|
|
197
|
-
// Keep a finite marker in every populated frame. It documents that the
|
|
198
|
-
// structured board/context read remains the place for complete task detail,
|
|
199
|
-
// and reserving it makes packing deterministic at the exact boundary.
|
|
200
|
-
const marker = '… full task details in board / get_context.';
|
|
201
|
-
const detailHeader = 'task details:';
|
|
202
|
-
let frame = `${protectedFrame}\n${detailHeader}`;
|
|
203
|
-
let omitted = false;
|
|
204
|
-
for (const task of tasks) {
|
|
205
|
-
const line = taskDetail(task);
|
|
206
|
-
const candidate = `${frame}\n${line}`;
|
|
207
|
-
const withMarker = `${candidate}\n${marker}`;
|
|
208
|
-
if (codePointLength(withMarker) <= limit) {
|
|
209
|
-
frame = candidate;
|
|
210
|
-
} else {
|
|
211
|
-
omitted = true;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// The marker is always useful, and the protected portion was already proven
|
|
216
|
-
// to fit. If there is not enough room for the detail header plus marker,
|
|
217
|
-
// return the protected fields alone rather than slicing them.
|
|
218
|
-
const marked = `${frame}\n${marker}`;
|
|
219
|
-
if (codePointLength(marked) <= limit) return marked;
|
|
220
|
-
if (omitted || codePointLength(`${protectedFrame}\n${detailHeader}\n${marker}`) > limit) {
|
|
221
|
-
return protectedFrame;
|
|
222
|
-
}
|
|
223
|
-
return frame;
|
|
224
|
-
};
|
package/src/lib/pod-tail.js
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Live pod tail for `commonly pod tail`.
|
|
3
|
-
*
|
|
4
|
-
* GET /api/messages/:podId takes `before` and `limit` only; it ignores `after`
|
|
5
|
-
* and returns the newest `limit` rows. Polling it with a cursor therefore
|
|
6
|
-
* re-serves the same window every tick. The poller keeps the ids it has
|
|
7
|
-
* already printed and prints only the rest, in the order the page returns
|
|
8
|
-
* them (oldest first).
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
// Ids kept for de-duplication. Old entries fall out first, so a long tail
|
|
12
|
-
// stays bounded in memory.
|
|
13
|
-
export const TAIL_SEEN_MAX = 1000;
|
|
14
|
-
export const TAIL_PAGE_LIMIT = 100;
|
|
15
|
-
|
|
16
|
-
// Same bot field the v2 chat reads (frontend/src/v2/utils/threadView.ts
|
|
17
|
-
// participantOf): `user.isBot`, falling back to the mapped `userId.isBot`.
|
|
18
|
-
export const isAgentMessage = (m) => {
|
|
19
|
-
if (typeof m?.user?.isBot === 'boolean') return m.user.isBot;
|
|
20
|
-
return m?.userId?.isBot === true;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
const authorName = (m) => m?.user?.username || m?.userId?.username || m?.username || '?';
|
|
24
|
-
|
|
25
|
-
const clock = (m) => {
|
|
26
|
-
const raw = m?.createdAt || m?.timestamp;
|
|
27
|
-
const date = raw ? new Date(raw) : null;
|
|
28
|
-
return date && !Number.isNaN(date.getTime()) ? date.toTimeString().slice(0, 8) : '--:--:--';
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
export const formatTailLine = (m) => {
|
|
32
|
-
const marker = isAgentMessage(m) ? '🤖' : '👤';
|
|
33
|
-
return `[${clock(m)}] ${marker} ${authorName(m)}: ${m?.content ?? ''}`;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* @param {object} opts
|
|
38
|
-
* @param {(limit: number) => Promise<object[] | {messages: object[]}>} opts.fetchPage
|
|
39
|
-
* @param {'all'|'agents'|'humans'} [opts.filter='all']
|
|
40
|
-
* @param {(line: string) => void} opts.print
|
|
41
|
-
*/
|
|
42
|
-
export const createTailPoller = ({ fetchPage, filter = 'all', print }) => {
|
|
43
|
-
const seen = new Set();
|
|
44
|
-
const remember = (id) => {
|
|
45
|
-
seen.add(id);
|
|
46
|
-
if (seen.size > TAIL_SEEN_MAX) seen.delete(seen.values().next().value);
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
return async () => {
|
|
50
|
-
const data = await fetchPage(TAIL_PAGE_LIMIT);
|
|
51
|
-
const messages = Array.isArray(data) ? data : data?.messages || [];
|
|
52
|
-
let printed = 0;
|
|
53
|
-
|
|
54
|
-
messages.forEach((m) => {
|
|
55
|
-
const id = String(m?._id || m?.id || '');
|
|
56
|
-
if (id) {
|
|
57
|
-
if (seen.has(id)) return;
|
|
58
|
-
remember(id);
|
|
59
|
-
}
|
|
60
|
-
if (filter === 'agents' && !isAgentMessage(m)) return;
|
|
61
|
-
if (filter === 'humans' && isAgentMessage(m)) return;
|
|
62
|
-
print(formatTailLine(m));
|
|
63
|
-
printed += 1;
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
return printed;
|
|
67
|
-
};
|
|
68
|
-
};
|
package/src/lib/poll-liveness.js
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Poll liveness for the inbox loops (TASK-338).
|
|
3
|
-
*
|
|
4
|
-
* The hard timeout in api.js bounds every CAP request, so no single request can
|
|
5
|
-
* hang for good any more. This is the second net, for a loop that stops making
|
|
6
|
-
* progress anyway: if no poll has settled within the limit while the loop was
|
|
7
|
-
* doing its own work, it is wedged, and the only thing that heals it is a fresh
|
|
8
|
-
* process. Measured on 10-31: nib's and folio's loops sat 8+ hours on one
|
|
9
|
-
* unanswered request, alive the whole time, and their keepalives (tmux, launchd
|
|
10
|
-
* KeepAlive) respawn only on exit. So a stall ends the process, loudly, and the
|
|
11
|
-
* keepalive does the rest.
|
|
12
|
-
*
|
|
13
|
-
* The clock runs while the loop is polling, acking or holding work, and is
|
|
14
|
-
* paused while a model turn runs and while the loop waits on its own scheduled
|
|
15
|
-
* retry. A two-hour turn never trips it, and neither does a fifteen-minute
|
|
16
|
-
* backoff: those always end on their own. A settled poll or a finished turn
|
|
17
|
-
* restarts the clock.
|
|
18
|
-
*
|
|
19
|
-
* Monotonic time, not Date.now: a laptop seat that sleeps, or a clock corrected
|
|
20
|
-
* by ten minutes, must not read as a stall.
|
|
21
|
-
*/
|
|
22
|
-
import { performance } from 'perf_hooks';
|
|
23
|
-
|
|
24
|
-
export const POLL_LIVENESS_DEFAULT_MS = 10 * 60 * 1000;
|
|
25
|
-
|
|
26
|
-
export const resolvePollLivenessMs = (value, env = process.env) => {
|
|
27
|
-
const candidate = [value, Number(env.COMMONLY_POLL_LIVENESS_MS)]
|
|
28
|
-
.find((ms) => Number.isFinite(ms) && ms > 0);
|
|
29
|
-
return candidate ?? POLL_LIVENESS_DEFAULT_MS;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
export const formatStall = (ms) => (ms < 60_000
|
|
33
|
-
? `${Math.max(1, Math.round(ms / 1000))}s`
|
|
34
|
-
: `${Math.round(ms / 60_000)} min`);
|
|
35
|
-
|
|
36
|
-
export const createPollLiveness = ({ limitMs, onStall, checkEveryMs }) => {
|
|
37
|
-
let armedSince = null;
|
|
38
|
-
let stalled = false;
|
|
39
|
-
// A quarter of the limit, so a stall is caught within 1.25x of it, bounded
|
|
40
|
-
// to a check a second at most and one a minute at least.
|
|
41
|
-
const every = Number.isFinite(checkEveryMs) && checkEveryMs > 0
|
|
42
|
-
? checkEveryMs
|
|
43
|
-
: Math.min(60_000, Math.max(1000, Math.floor(limitMs / 4)));
|
|
44
|
-
const timer = setInterval(() => {
|
|
45
|
-
if (stalled || armedSince === null) return;
|
|
46
|
-
const stalledMs = performance.now() - armedSince;
|
|
47
|
-
if (stalledMs < limitMs) return;
|
|
48
|
-
stalled = true;
|
|
49
|
-
onStall({ stalledMs, limitMs });
|
|
50
|
-
}, every);
|
|
51
|
-
// The check must never be what keeps a stopped loop's process alive.
|
|
52
|
-
timer.unref?.();
|
|
53
|
-
return {
|
|
54
|
-
// (Re)start the clock: the loop is doing its own work, or just made progress.
|
|
55
|
-
arm: () => { armedSince = performance.now(); },
|
|
56
|
-
// Stop the clock: a model turn, or a wait the loop scheduled itself.
|
|
57
|
-
pause: () => { armedSince = null; },
|
|
58
|
-
stop: () => { clearInterval(timer); },
|
|
59
|
-
};
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
export const IDLE_LINE_DEFAULT_MS = 10 * 60 * 1000;
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* A healthy idle seat and a wedged one used to look the same in a log: both
|
|
66
|
-
* printed nothing. On 11-10 six seats sat silent for 4.6 hours after a
|
|
67
|
-
* `fetch failed` and were restarted as wedged, though nothing showed they had
|
|
68
|
-
* stopped polling. So a seat that keeps polling and finds nothing says so on a
|
|
69
|
-
* slow cadence, `polling ok (N empty polls since HH:MMZ)`. While a seat is idle
|
|
70
|
-
* (no turn running, no retry wait), no line for two cadences means the loop has
|
|
71
|
-
* stopped.
|
|
72
|
-
*
|
|
73
|
-
* `empty()` after each poll that settled with nothing to do; `busy()` after one
|
|
74
|
-
* that brought work or failed, which starts the next count, so a window never
|
|
75
|
-
* spans an outage. The first line comes one
|
|
76
|
-
* cadence after the seat goes quiet, never on start.
|
|
77
|
-
*/
|
|
78
|
-
export const createIdleHeartbeat = ({ everyMs = IDLE_LINE_DEFAULT_MS, log, now = Date.now }) => {
|
|
79
|
-
let since = null;
|
|
80
|
-
let count = 0;
|
|
81
|
-
const hhmm = (ms) => `${new Date(ms).toISOString().slice(11, 16)}Z`;
|
|
82
|
-
return {
|
|
83
|
-
empty: () => {
|
|
84
|
-
const at = now();
|
|
85
|
-
if (since === null) since = at;
|
|
86
|
-
count += 1;
|
|
87
|
-
if (at - since < everyMs) return;
|
|
88
|
-
log(`polling ok (${count} empty poll${count === 1 ? '' : 's'} since ${hhmm(since)})`);
|
|
89
|
-
since = null;
|
|
90
|
-
count = 0;
|
|
91
|
-
},
|
|
92
|
-
busy: () => {
|
|
93
|
-
since = null;
|
|
94
|
-
count = 0;
|
|
95
|
-
},
|
|
96
|
-
};
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
export const resolveIdleLineMs = (value, env = process.env) => {
|
|
100
|
-
const candidate = [value, Number(env.COMMONLY_IDLE_LINE_MS)]
|
|
101
|
-
.find((ms) => Number.isFinite(ms) && ms > 0);
|
|
102
|
-
return candidate ?? IDLE_LINE_DEFAULT_MS;
|
|
103
|
-
};
|
package/src/lib/poll-retry.js
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Retry policy for the run loop's own FETCH failures (TASK-025).
|
|
3
|
-
*
|
|
4
|
-
* `spawn-retry.js` bounds failures of the subprocess a poll produces. This
|
|
5
|
-
* bounds failures of the poll itself — the request to
|
|
6
|
-
* `/api/agents/runtime/events` that has to succeed before there is anything to
|
|
7
|
-
* spawn. They are different failures with different remedies and, until now,
|
|
8
|
-
* only one of them had a policy.
|
|
9
|
-
*
|
|
10
|
-
* The gap, measured: `agent run`'s tick set `nextPollDelayMs = intervalMs` and
|
|
11
|
-
* only ever reassigned it inside the spawn-retry branch. A fetch that threw
|
|
12
|
-
* never reached that branch, so a network failure retried at a flat 5s
|
|
13
|
-
* forever — no backoff, no ceiling, and no escalation. 797 consecutive
|
|
14
|
-
* `fetch failed` ran across three seats that way: ~66 minutes at 5s, and the
|
|
15
|
-
* only trace was one `onError` line per attempt in a log nobody was tailing.
|
|
16
|
-
*
|
|
17
|
-
* Two deliberate differences from the auth path directly above it:
|
|
18
|
-
*
|
|
19
|
-
* 1. NO STOP. `MAX_AUTH_ERRORS` halts the loop because a rejected token does
|
|
20
|
-
* not heal on its own — retrying is pure cost and the operator must act.
|
|
21
|
-
* A network failure is usually transient, and a seat that stops on one is
|
|
22
|
-
* dead until someone notices. So this backs off and stays alive.
|
|
23
|
-
*
|
|
24
|
-
* 2. ESCALATION IS THE POINT. The harm in the measured outage was not the
|
|
25
|
-
* retry rate, it was that 797 failures produced no signal distinguishable
|
|
26
|
-
* from one failure. `escalate` fires on a small set of thresholds so a
|
|
27
|
-
* sustained outage announces itself at widening intervals instead of
|
|
28
|
-
* disappearing into per-attempt noise.
|
|
29
|
-
*/
|
|
30
|
-
|
|
31
|
-
// Same ceiling as the spawn path: past this, more waiting buys nothing and a
|
|
32
|
-
// human is the only thing that resolves it.
|
|
33
|
-
export const POLL_RETRY_MAX_MS = 15 * 60 * 1000;
|
|
34
|
-
|
|
35
|
-
// Backoff starts only after the first failure has been retried once at the
|
|
36
|
-
// normal interval, so a single blip costs nothing.
|
|
37
|
-
export const POLL_BACKOFF_AFTER = 1;
|
|
38
|
-
|
|
39
|
-
// Failure counts that emit a loud line. Chosen against the measured outage:
|
|
40
|
-
// at intervalMs=5000 these land at roughly 15s, 1min, 5min, 20min and 1h of
|
|
41
|
-
// sustained failure, so an operator reading the log sees the shape of the
|
|
42
|
-
// outage rather than 797 identical lines.
|
|
43
|
-
export const POLL_ESCALATE_AT = Object.freeze([3, 10, 30, 60, 120]);
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Next poll delay after a failed fetch, and whether this attempt should be
|
|
47
|
-
* announced loudly.
|
|
48
|
-
*
|
|
49
|
-
* Exponential from `intervalMs`, bounded by POLL_RETRY_MAX_MS. Jitter is the
|
|
50
|
-
* same anti-herd offset the spawn path uses — without it, every seat that lost
|
|
51
|
-
* the same upstream retries in lockstep and re-creates the thundering herd on
|
|
52
|
-
* recovery.
|
|
53
|
-
*/
|
|
54
|
-
export const pollRetryPolicy = ({
|
|
55
|
-
consecutiveFailures,
|
|
56
|
-
intervalMs,
|
|
57
|
-
jitterRatio = 0,
|
|
58
|
-
}) => {
|
|
59
|
-
const safeIntervalMs = Number.isFinite(intervalMs) && intervalMs > 0 ? intervalMs : 5000;
|
|
60
|
-
const failureCount = Number.isInteger(consecutiveFailures) && consecutiveFailures > 0
|
|
61
|
-
? consecutiveFailures
|
|
62
|
-
: 1;
|
|
63
|
-
|
|
64
|
-
const steps = Math.max(0, failureCount - POLL_BACKOFF_AFTER);
|
|
65
|
-
const raw = safeIntervalMs * (2 ** steps);
|
|
66
|
-
const bounded = Math.min(POLL_RETRY_MAX_MS, raw);
|
|
67
|
-
|
|
68
|
-
// Clamp rather than trust the caller: a jitterRatio above the cap would
|
|
69
|
-
// widen the herd window instead of narrowing it.
|
|
70
|
-
const safeJitter = Number.isFinite(jitterRatio)
|
|
71
|
-
? Math.min(0.2, Math.max(0, jitterRatio))
|
|
72
|
-
: 0;
|
|
73
|
-
|
|
74
|
-
return {
|
|
75
|
-
delayMs: Math.round(bounded * (1 + safeJitter)),
|
|
76
|
-
escalate: POLL_ESCALATE_AT.includes(failureCount),
|
|
77
|
-
atCeiling: bounded >= POLL_RETRY_MAX_MS,
|
|
78
|
-
};
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
export default pollRetryPolicy;
|