@huseynsnmz/pi-suite 0.0.0-stage → 0.2.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/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.md +88 -2
- package/SECURITY.md +5 -0
- package/package.json +112 -4
- package/packages/board/CHANGELOG.md +340 -0
- package/packages/board/CONTRIBUTING.md +7 -0
- package/packages/board/LICENSE +21 -0
- package/packages/board/README.md +88 -0
- package/packages/board/SECURITY.md +19 -0
- package/packages/board/docs/how-it-works.md +31 -0
- package/packages/board/docs/reference.md +278 -0
- package/packages/board/package.json +81 -0
- package/packages/board/runner/job-runner-legacy.mjs +68 -0
- package/packages/board/runner/job-runner.mjs +834 -0
- package/packages/board/runner/pty-runner-legacy.mjs +50 -0
- package/packages/board/runner/pty-runner.mjs +2175 -0
- package/packages/board/runner/state-coordinator.mjs +673 -0
- package/packages/board/runner/state-runner.mjs +203 -0
- package/packages/board/runner/title-runner.mjs +80 -0
- package/packages/board/src/commands/agent-board.ts +394 -0
- package/packages/board/src/commands/attach-decision.mjs +66 -0
- package/packages/board/src/commands/attach-flow.ts +251 -0
- package/packages/board/src/commands/bg.ts +103 -0
- package/packages/board/src/commands/board-model.ts +21 -0
- package/packages/board/src/core/atomic.mjs +184 -0
- package/packages/board/src/core/attach-delete.mjs +24 -0
- package/packages/board/src/core/attach-frame-proof.mjs +870 -0
- package/packages/board/src/core/attach-frame-reporter.mjs +648 -0
- package/packages/board/src/core/attach-frame-stream.mjs +266 -0
- package/packages/board/src/core/attach-view-proof.mjs +709 -0
- package/packages/board/src/core/auto-state.mjs +377 -0
- package/packages/board/src/core/background-runs.mjs +273 -0
- package/packages/board/src/core/code-refs-store.mjs +315 -0
- package/packages/board/src/core/code-refs.mjs +913 -0
- package/packages/board/src/core/config.mjs +77 -0
- package/packages/board/src/core/control-protocol.mjs +482 -0
- package/packages/board/src/core/coordinator-client.mjs +633 -0
- package/packages/board/src/core/coordinator-journal.mjs +282 -0
- package/packages/board/src/core/coordinator-protocol.mjs +14 -0
- package/packages/board/src/core/cwd-stats.mjs +114 -0
- package/packages/board/src/core/dashboard-render.mjs +10 -0
- package/packages/board/src/core/derive.mjs +117 -0
- package/packages/board/src/core/diagnostics.mjs +133 -0
- package/packages/board/src/core/double-press.mjs +26 -0
- package/packages/board/src/core/editor-state-reporter.mjs +112 -0
- package/packages/board/src/core/events.mjs +286 -0
- package/packages/board/src/core/evidence.mjs +245 -0
- package/packages/board/src/core/follow-up-queue.mjs +211 -0
- package/packages/board/src/core/foreground-preview-cache.mjs +117 -0
- package/packages/board/src/core/foreground-projection-cache.mjs +99 -0
- package/packages/board/src/core/frame-witness-update.mjs +35 -0
- package/packages/board/src/core/heuristics.mjs +313 -0
- package/packages/board/src/core/hold-input.mjs +27 -0
- package/packages/board/src/core/host-coordination.mjs +300 -0
- package/packages/board/src/core/host-crash.mjs +79 -0
- package/packages/board/src/core/host-probe.mjs +207 -0
- package/packages/board/src/core/host-protocol.mjs +73 -0
- package/packages/board/src/core/ids.mjs +35 -0
- package/packages/board/src/core/ime-cursor-coalesce.mjs +193 -0
- package/packages/board/src/core/invocation.mjs +43 -0
- package/packages/board/src/core/launch-options.mjs +418 -0
- package/packages/board/src/core/launch.mjs +157 -0
- package/packages/board/src/core/load-config.mjs +4 -0
- package/packages/board/src/core/locks.mjs +530 -0
- package/packages/board/src/core/paths.mjs +183 -0
- package/packages/board/src/core/pid.mjs +75 -0
- package/packages/board/src/core/prewarm-schedule.mjs +41 -0
- package/packages/board/src/core/process-identity-probe.mjs +228 -0
- package/packages/board/src/core/prompt-transport.mjs +13 -0
- package/packages/board/src/core/pty-attach-jiggle-controller.mjs +356 -0
- package/packages/board/src/core/pty-attach-jiggle-retry.mjs +109 -0
- package/packages/board/src/core/pty-attach-reconnect.mjs +61 -0
- package/packages/board/src/core/pty-attach-render.mjs +229 -0
- package/packages/board/src/core/pty-links.mjs +81 -0
- package/packages/board/src/core/pty-scroll.mjs +522 -0
- package/packages/board/src/core/pty-support.mjs +327 -0
- package/packages/board/src/core/repo.mjs +103 -0
- package/packages/board/src/core/rows.mjs +414 -0
- package/packages/board/src/core/screen-log-gc.mjs +198 -0
- package/packages/board/src/core/screen-log.mjs +160 -0
- package/packages/board/src/core/session-view.mjs +174 -0
- package/packages/board/src/core/slash-commands.mjs +52 -0
- package/packages/board/src/core/state-commands.mjs +757 -0
- package/packages/board/src/core/status-consistency.mjs +98 -0
- package/packages/board/src/core/steering-prompts.mjs +34 -0
- package/packages/board/src/core/steering.mjs +133 -0
- package/packages/board/src/core/stop-latch.mjs +41 -0
- package/packages/board/src/core/store.mjs +523 -0
- package/packages/board/src/core/suspend.mjs +36 -0
- package/packages/board/src/core/terminal-attach-client.mjs +981 -0
- package/packages/board/src/core/terminal-attach-protocol.mjs +460 -0
- package/packages/board/src/core/terminal-model.mjs +222 -0
- package/packages/board/src/core/terminal-query-sequences.mjs +239 -0
- package/packages/board/src/core/terminal-reply-guard.mjs +222 -0
- package/packages/board/src/core/terminal-snapshot.mjs +440 -0
- package/packages/board/src/core/test-knobs.mjs +28 -0
- package/packages/board/src/core/title.mjs +43 -0
- package/packages/board/src/core/types.mjs +414 -0
- package/packages/board/src/core/warm-host-sweeper.mjs +168 -0
- package/packages/board/src/core/worktree.mjs +65 -0
- package/packages/board/src/index.ts +303 -0
- package/packages/board/src/runtime/service.mjs +3222 -0
- package/packages/board/src/ui/chrome.ts +20 -0
- package/packages/board/src/ui/dashboard-decisions.mjs +55 -0
- package/packages/board/src/ui/dashboard-evidence.mjs +85 -0
- package/packages/board/src/ui/dashboard.ts +2237 -0
- package/packages/board/src/ui/double-left.ts +30 -0
- package/packages/board/src/ui/external-editor.ts +25 -0
- package/packages/board/src/ui/focus.ts +32 -0
- package/packages/board/src/ui/pty-attach.ts +3184 -0
- package/packages/board/src/ui/waiting.ts +30 -0
- package/packages/commands/LICENSE +21 -0
- package/packages/commands/README.md +19 -0
- package/packages/commands/package.json +22 -0
- package/packages/commands/src/index.ts +43 -0
- package/packages/commands/src/security-review.ts +109 -0
- package/packages/frame/CHANGELOG.md +113 -0
- package/packages/frame/CONTRIBUTING.md +7 -0
- package/packages/frame/LICENSE +22 -0
- package/packages/frame/README.md +105 -0
- package/packages/frame/SECURITY.md +16 -0
- package/packages/frame/docs/how-it-works.md +53 -0
- package/packages/frame/docs/reference.md +78 -0
- package/packages/frame/package.json +72 -0
- package/packages/frame/src/data/git.ts +90 -0
- package/packages/frame/src/data/github.ts +140 -0
- package/packages/frame/src/data/gitlab.ts +63 -0
- package/packages/frame/src/data/mode.ts +49 -0
- package/packages/frame/src/data/past-prompts.ts +57 -0
- package/packages/frame/src/data/runner.ts +32 -0
- package/packages/frame/src/data/snapshot.ts +223 -0
- package/packages/frame/src/data/start-notes.ts +104 -0
- package/packages/frame/src/data/telemetry.ts +337 -0
- package/packages/frame/src/data/todos.ts +162 -0
- package/packages/frame/src/data/totals.ts +94 -0
- package/packages/frame/src/data/visits.ts +45 -0
- package/packages/frame/src/index.ts +696 -0
- package/packages/frame/src/kit/LICENSE +21 -0
- package/packages/frame/src/kit/app/attribution.ts +30 -0
- package/packages/frame/src/kit/app/builder.ts +186 -0
- package/packages/frame/src/kit/app/claim.ts +27 -0
- package/packages/frame/src/kit/app/feature.ts +91 -0
- package/packages/frame/src/kit/app/host.ts +214 -0
- package/packages/frame/src/kit/app/renderers.ts +62 -0
- package/packages/frame/src/kit/app/scope.ts +125 -0
- package/packages/frame/src/kit/board/model.ts +337 -0
- package/packages/frame/src/kit/contract.ts +79 -0
- package/packages/frame/src/kit/contracts/accounts.ts +33 -0
- package/packages/frame/src/kit/contracts/ask.ts +120 -0
- package/packages/frame/src/kit/contracts/board.ts +6 -0
- package/packages/frame/src/kit/contracts/by.ts +22 -0
- package/packages/frame/src/kit/contracts/calls.ts +46 -0
- package/packages/frame/src/kit/contracts/events.ts +89 -0
- package/packages/frame/src/kit/contracts/mode.ts +26 -0
- package/packages/frame/src/kit/contracts/names.ts +12 -0
- package/packages/frame/src/kit/contracts/peers.ts +77 -0
- package/packages/frame/src/kit/contracts/prompts.ts +8 -0
- package/packages/frame/src/kit/contracts/screen.ts +75 -0
- package/packages/frame/src/kit/contracts/skills.ts +81 -0
- package/packages/frame/src/kit/contracts/sources.ts +81 -0
- package/packages/frame/src/kit/contracts/views.ts +42 -0
- package/packages/frame/src/kit/control.ts +59 -0
- package/packages/frame/src/kit/dark-theme.ts +41 -0
- package/packages/frame/src/kit/decode.ts +210 -0
- package/packages/frame/src/kit/events/log.ts +313 -0
- package/packages/frame/src/kit/format.ts +25 -0
- package/packages/frame/src/kit/host.ts +4 -0
- package/packages/frame/src/kit/index.ts +25 -0
- package/packages/frame/src/kit/legacy-tasks.ts +39 -0
- package/packages/frame/src/kit/overlay-images.ts +36 -0
- package/packages/frame/src/kit/paths.ts +16 -0
- package/packages/frame/src/kit/screen/client.ts +74 -0
- package/packages/frame/src/kit/screen/feature.ts +64 -0
- package/packages/frame/src/kit/screen/model.ts +182 -0
- package/packages/frame/src/kit/screen/view.ts +838 -0
- package/packages/frame/src/kit/settings/files.ts +90 -0
- package/packages/frame/src/kit/settings/setting.ts +57 -0
- package/packages/frame/src/kit/settings/store.ts +192 -0
- package/packages/frame/src/kit/sources/file.ts +129 -0
- package/packages/frame/src/kit/sources/follow.ts +155 -0
- package/packages/frame/src/kit/testing.ts +260 -0
- package/packages/frame/src/kit/totals.ts +72 -0
- package/packages/frame/src/kit/ui/entry.ts +75 -0
- package/packages/frame/src/kit/ui/frame.ts +212 -0
- package/packages/frame/src/kit/ui/glyphs.ts +132 -0
- package/packages/frame/src/kit/ui/scroll.ts +14 -0
- package/packages/frame/src/render/diff.ts +157 -0
- package/packages/frame/src/render/fit.ts +86 -0
- package/packages/frame/src/render/format.ts +108 -0
- package/packages/frame/src/render/frame.ts +106 -0
- package/packages/frame/src/render/glyphs.ts +189 -0
- package/packages/frame/src/render/keys.ts +43 -0
- package/packages/frame/src/render/lead.ts +37 -0
- package/packages/frame/src/render/paint.ts +135 -0
- package/packages/frame/src/render/safe.ts +19 -0
- package/packages/frame/src/render/segments.ts +735 -0
- package/packages/frame/src/render/streaming.ts +223 -0
- package/packages/frame/src/render/timing.ts +106 -0
- package/packages/frame/src/render/tools.ts +167 -0
- package/packages/frame/src/settings.ts +105 -0
- package/packages/frame/src/ui/editor.ts +272 -0
- package/packages/frame/src/ui/effort.ts +36 -0
- package/packages/frame/src/ui/footer.ts +165 -0
- package/packages/frame/src/ui/header.ts +362 -0
- package/packages/frame/src/ui/history-search.ts +23 -0
- package/packages/frame/src/ui/interrupt.ts +21 -0
- package/packages/frame/src/ui/outputs.ts +170 -0
- package/packages/frame/src/ui/pi-entries.ts +298 -0
- package/packages/frame/src/ui/previews.ts +72 -0
- package/packages/frame/src/ui/queue.ts +75 -0
- package/packages/frame/src/ui/screen.ts +59 -0
- package/packages/frame/src/ui/timings.ts +48 -0
- package/packages/frame/src/ui/todos.ts +205 -0
- package/packages/frame/src/ui/tool-frame.ts +595 -0
- package/packages/frame/src/ui/working.ts +25 -0
- package/packages/frame/src/view/state.ts +97 -0
- package/packages/frame/src/view/transcript.ts +175 -0
- package/packages/frame/src/view/view.ts +162 -0
- package/packages/memory/CONTRIBUTING.md +7 -0
- package/packages/memory/LICENSE +21 -0
- package/packages/memory/README.md +44 -0
- package/packages/memory/SECURITY.md +20 -0
- package/packages/memory/package.json +55 -0
- package/packages/memory/skills/review-memory/SKILL.md +48 -0
- package/packages/memory/src/index.ts +163 -0
- package/packages/memory/src/prompt.ts +25 -0
- package/packages/memory/src/review.ts +150 -0
- package/packages/memory/src/secrets.ts +22 -0
- package/packages/memory/src/store.ts +173 -0
- package/packages/modes/CHANGELOG.md +104 -0
- package/packages/modes/CONTRIBUTING.md +7 -0
- package/packages/modes/LICENSE +21 -0
- package/packages/modes/README.md +174 -0
- package/packages/modes/SECURITY.md +17 -0
- package/packages/modes/package.json +55 -0
- package/packages/modes/skills/review-permissions/SKILL.md +54 -0
- package/packages/modes/src/bash.ts +218 -0
- package/packages/modes/src/deny-off.ts +62 -0
- package/packages/modes/src/git-safety.ts +72 -0
- package/packages/modes/src/index.ts +338 -0
- package/packages/modes/src/interview.ts +206 -0
- package/packages/modes/src/judge-gate.ts +113 -0
- package/packages/modes/src/modes.ts +121 -0
- package/packages/modes/src/review/apply.mjs +156 -0
- package/packages/modes/src/review/collect.mjs +208 -0
- package/packages/modes/src/review/tools.ts +71 -0
- package/packages/modes/src/sandbox.ts +37 -0
- package/packages/router/CHANGELOG.md +8 -0
- package/packages/router/LICENSE +21 -0
- package/packages/router/README.md +90 -0
- package/packages/router/package.json +51 -0
- package/packages/router/src/config.ts +65 -0
- package/packages/router/src/index.ts +172 -0
- package/packages/router/src/judge.ts +51 -0
- package/packages/router/src/route.ts +67 -0
- package/packages/web/CHANGELOG.md +36 -0
- package/packages/web/LICENSE +21 -0
- package/packages/web/README.md +87 -0
- package/packages/web/SECURITY.md +13 -0
- package/packages/web/package.json +59 -0
- package/packages/web/src/config.ts +109 -0
- package/packages/web/src/dnsfilter.ts +97 -0
- package/packages/web/src/extract.ts +65 -0
- package/packages/web/src/index.ts +217 -0
- package/packages/web/src/markdown.ts +122 -0
- package/packages/web/src/net.ts +109 -0
- package/packages/web/src/reader.ts +73 -0
- package/packages/web/src/sanitize.ts +50 -0
- package/packages/web/src/search.ts +76 -0
- package/packages/web/src/secrets.ts +22 -0
- package/packages/web/src/validate.ts +186 -0
- package/src/index.ts +156 -0
- package/src/settings.ts +79 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/** Durable FIFO follow-up queue helpers. */
|
|
2
|
+
import { atomicWriteJson, readJson } from "./atomic.mjs";
|
|
3
|
+
import { newFollowUpId } from "./ids.mjs";
|
|
4
|
+
import { truncate } from "./heuristics.mjs";
|
|
5
|
+
import { withViewLockSync } from "./locks.mjs";
|
|
6
|
+
import * as P from "./paths.mjs";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Run a queue mutation under the view lock, translating any failure (lock
|
|
10
|
+
* unavailable, fs errors inside the mutation) into {ok:false} so callers on
|
|
11
|
+
* the {ok} convention never see a throw (issue #33).
|
|
12
|
+
* @template T
|
|
13
|
+
* @param {string} root
|
|
14
|
+
* @param {string} viewId
|
|
15
|
+
* @param {() => T} fn
|
|
16
|
+
* @returns {T | { ok: false, error: string }}
|
|
17
|
+
*/
|
|
18
|
+
function lockedQueueOp(root, viewId, fn) {
|
|
19
|
+
try {
|
|
20
|
+
return withViewLockSync(root, viewId, "queue", fn);
|
|
21
|
+
} catch (err) {
|
|
22
|
+
return { ok: false, error: `follow-up queue lock unavailable: ${err instanceof Error ? err.message : String(err)}` };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** @param {string} viewId @param {number} [now] @returns {import("./types.mjs").FollowUpQueue} */
|
|
27
|
+
export function emptyFollowUpQueue(viewId, now = Date.now()) {
|
|
28
|
+
return { version: 1, viewId, nextSeq: 1, updatedAt: now, items: [] };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** @param {string} root @param {string} viewId */
|
|
32
|
+
export function readFollowUpQueue(root, viewId) {
|
|
33
|
+
return normalizeQueue(readJson(P.followUpQueuePath(root, viewId), null), viewId);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** @param {string} root @param {import("./types.mjs").FollowUpQueue} queue */
|
|
37
|
+
export function writeFollowUpQueue(root, queue) {
|
|
38
|
+
const normalized = normalizeQueue(queue, queue.viewId);
|
|
39
|
+
normalized.updatedAt = Date.now();
|
|
40
|
+
atomicWriteJson(P.followUpQueuePath(root, normalized.viewId), normalized);
|
|
41
|
+
return normalized;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** @param {import("./types.mjs").FollowUpQueue} queue @returns {import("./types.mjs").FollowUpSummary} */
|
|
45
|
+
export function summarizeFollowUpQueue(queue) {
|
|
46
|
+
const q = normalizeQueue(queue, queue?.viewId ?? "");
|
|
47
|
+
const queued = q.items.filter((i) => i.status === "queued");
|
|
48
|
+
const claimed = q.items.filter((i) => i.status === "claimed");
|
|
49
|
+
const last = [...queued].sort((a, b) => b.createdAt - a.createdAt)[0] ?? null;
|
|
50
|
+
return {
|
|
51
|
+
queuedCount: queued.length,
|
|
52
|
+
claimedCount: claimed.length,
|
|
53
|
+
lastQueuedAt: last?.createdAt ?? null,
|
|
54
|
+
lastQueuedPreview: last ? truncate(last.text, 120) : null,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** @param {string} root @param {string} viewId @param {string} text @param {{ kind?: import("./types.mjs").FollowUpKind, source?: string, delivery?: "auto"|"now"|"queue" }} [opts] */
|
|
59
|
+
export function enqueueFollowUp(root, viewId, text, opts = {}) {
|
|
60
|
+
const clean = String(text || "").trim();
|
|
61
|
+
if (!clean) return { ok: false, error: "Empty follow-up" };
|
|
62
|
+
return lockedQueueOp(root, viewId, () => {
|
|
63
|
+
const queue = readFollowUpQueue(root, viewId);
|
|
64
|
+
const now = Date.now();
|
|
65
|
+
const item = {
|
|
66
|
+
id: newFollowUpId(),
|
|
67
|
+
seq: queue.nextSeq,
|
|
68
|
+
viewId,
|
|
69
|
+
kind: opts.kind ?? "reply",
|
|
70
|
+
text: clean,
|
|
71
|
+
createdAt: now,
|
|
72
|
+
updatedAt: now,
|
|
73
|
+
status: "queued",
|
|
74
|
+
source: opts.source ?? "user",
|
|
75
|
+
delivery: opts.delivery ?? "auto",
|
|
76
|
+
runId: null,
|
|
77
|
+
claimedAt: null,
|
|
78
|
+
completedAt: null,
|
|
79
|
+
attempts: 0,
|
|
80
|
+
error: null,
|
|
81
|
+
};
|
|
82
|
+
queue.nextSeq += 1;
|
|
83
|
+
queue.items.push(item);
|
|
84
|
+
writeFollowUpQueue(root, queue);
|
|
85
|
+
return { ok: true, item, summary: summarizeFollowUpQueue(queue) };
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** @param {string} root @param {string} viewId @param {{ runId?: string|null }} [opts] */
|
|
90
|
+
export function claimNextFollowUp(root, viewId, opts = {}) {
|
|
91
|
+
return lockedQueueOp(root, viewId, () => {
|
|
92
|
+
const queue = readFollowUpQueue(root, viewId);
|
|
93
|
+
const item = queue.items.filter((i) => i.status === "queued").sort((a, b) => a.seq - b.seq)[0];
|
|
94
|
+
if (!item) return { ok: false, error: "No queued follow-up" };
|
|
95
|
+
item.status = "claimed";
|
|
96
|
+
item.claimedAt = Date.now();
|
|
97
|
+
item.updatedAt = item.claimedAt;
|
|
98
|
+
item.attempts = (item.attempts ?? 0) + 1;
|
|
99
|
+
item.runId = opts.runId ?? item.runId ?? null;
|
|
100
|
+
writeFollowUpQueue(root, queue);
|
|
101
|
+
return { ok: true, item, summary: summarizeFollowUpQueue(queue) };
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** @param {string} root @param {string} viewId @param {string} itemId @param {{ runId?: string|null }} [opts] */
|
|
106
|
+
export function completeFollowUp(root, viewId, itemId, opts = {}) {
|
|
107
|
+
return updateItem(root, viewId, itemId, (item) => {
|
|
108
|
+
item.status = "completed";
|
|
109
|
+
item.completedAt = Date.now();
|
|
110
|
+
item.updatedAt = item.completedAt;
|
|
111
|
+
if (opts.runId) item.runId = opts.runId;
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** @param {string} root @param {string} viewId @param {string} itemId @param {string} error */
|
|
116
|
+
export function failFollowUp(root, viewId, itemId, error) {
|
|
117
|
+
return updateItem(root, viewId, itemId, (item) => {
|
|
118
|
+
item.status = "failed";
|
|
119
|
+
item.error = String(error || "Follow-up failed");
|
|
120
|
+
item.updatedAt = Date.now();
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** @param {string} root @param {string} viewId @param {string} itemId */
|
|
125
|
+
export function releaseFollowUp(root, viewId, itemId) {
|
|
126
|
+
return updateItem(root, viewId, itemId, (item) => {
|
|
127
|
+
item.status = "queued";
|
|
128
|
+
item.claimedAt = null;
|
|
129
|
+
item.updatedAt = Date.now();
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** @param {string} root @param {string} viewId */
|
|
134
|
+
export function removeLastFollowUp(root, viewId) {
|
|
135
|
+
return lockedQueueOp(root, viewId, () => {
|
|
136
|
+
const queue = readFollowUpQueue(root, viewId);
|
|
137
|
+
const queued = queue.items.filter((i) => i.status === "queued").sort((a, b) => b.seq - a.seq);
|
|
138
|
+
const last = queued[0];
|
|
139
|
+
if (!last) return { ok: false, error: "No queued follow-up" };
|
|
140
|
+
last.status = "cancelled";
|
|
141
|
+
last.updatedAt = Date.now();
|
|
142
|
+
writeFollowUpQueue(root, queue);
|
|
143
|
+
return { ok: true, item: last, summary: summarizeFollowUpQueue(queue) };
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** @param {string} root @param {string} viewId */
|
|
148
|
+
export function clearQueuedFollowUps(root, viewId) {
|
|
149
|
+
return lockedQueueOp(root, viewId, () => {
|
|
150
|
+
const queue = readFollowUpQueue(root, viewId);
|
|
151
|
+
let cancelled = 0;
|
|
152
|
+
for (const item of queue.items) {
|
|
153
|
+
if (item.status === "queued") {
|
|
154
|
+
item.status = "cancelled";
|
|
155
|
+
item.updatedAt = Date.now();
|
|
156
|
+
cancelled += 1;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
writeFollowUpQueue(root, queue);
|
|
160
|
+
return { ok: true, cancelled, summary: summarizeFollowUpQueue(queue) };
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** @param {string} root @param {string} viewId @param {string} itemId @param {(item: import("./types.mjs").FollowUpItem) => void} mutate */
|
|
165
|
+
function updateItem(root, viewId, itemId, mutate) {
|
|
166
|
+
return lockedQueueOp(root, viewId, () => {
|
|
167
|
+
const queue = readFollowUpQueue(root, viewId);
|
|
168
|
+
const item = queue.items.find((i) => i.id === itemId);
|
|
169
|
+
if (!item) return { ok: false, error: "Unknown follow-up" };
|
|
170
|
+
mutate(item);
|
|
171
|
+
writeFollowUpQueue(root, queue);
|
|
172
|
+
return { ok: true, item, summary: summarizeFollowUpQueue(queue) };
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** @param {any} queue @param {string} viewId @returns {import("./types.mjs").FollowUpQueue} */
|
|
177
|
+
function normalizeQueue(queue, viewId) {
|
|
178
|
+
const base = emptyFollowUpQueue(viewId || queue?.viewId || "");
|
|
179
|
+
if (!queue || typeof queue !== "object") return base;
|
|
180
|
+
const items = Array.isArray(queue.items) ? queue.items.map((item, idx) => normalizeItem(item, viewId, idx + 1)) : [];
|
|
181
|
+
return {
|
|
182
|
+
version: 1,
|
|
183
|
+
viewId: typeof queue.viewId === "string" ? queue.viewId : base.viewId,
|
|
184
|
+
nextSeq: Math.max(Number(queue.nextSeq ?? 1) || 1, items.reduce((m, i) => Math.max(m, i.seq + 1), 1)),
|
|
185
|
+
updatedAt: Number(queue.updatedAt ?? Date.now()) || Date.now(),
|
|
186
|
+
items,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** @param {any} item @param {string} viewId @param {number} seq */
|
|
191
|
+
function normalizeItem(item, viewId, seq) {
|
|
192
|
+
const now = Date.now();
|
|
193
|
+
const status = ["queued", "claimed", "completed", "failed", "cancelled"].includes(item?.status) ? item.status : "queued";
|
|
194
|
+
return {
|
|
195
|
+
id: typeof item?.id === "string" ? item.id : newFollowUpId(),
|
|
196
|
+
seq: Number(item?.seq ?? seq) || seq,
|
|
197
|
+
viewId: typeof item?.viewId === "string" ? item.viewId : viewId,
|
|
198
|
+
kind: ["reply", "plan_request", "plan_approval", "plan_change"].includes(item?.kind) ? item.kind : "reply",
|
|
199
|
+
text: String(item?.text ?? ""),
|
|
200
|
+
createdAt: Number(item?.createdAt ?? now) || now,
|
|
201
|
+
updatedAt: Number(item?.updatedAt ?? now) || now,
|
|
202
|
+
status,
|
|
203
|
+
source: typeof item?.source === "string" ? item.source : "user",
|
|
204
|
+
delivery: ["auto", "now", "queue"].includes(item?.delivery) ? item.delivery : "auto",
|
|
205
|
+
runId: typeof item?.runId === "string" ? item.runId : null,
|
|
206
|
+
claimedAt: Number.isFinite(item?.claimedAt) ? item.claimedAt : null,
|
|
207
|
+
completedAt: Number.isFinite(item?.completedAt) ? item.completedAt : null,
|
|
208
|
+
attempts: Number(item?.attempts ?? 0) || 0,
|
|
209
|
+
error: typeof item?.error === "string" ? item.error : null,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-local read-your-writes cache for foreground state projections
|
|
3
|
+
* (issue #113).
|
|
4
|
+
*
|
|
5
|
+
* Foreground turns are mirrored into state.json through the detached View
|
|
6
|
+
* State Coordinator: `message_end` sets `latestAssistantPreview` /
|
|
7
|
+
* `lastAgentActivityAt` and fires a fire-and-forget `sync_foreground` command;
|
|
8
|
+
* the coordinator journals + fsyncs before materializing the file. The next
|
|
9
|
+
* event (`agent_end`, ~7ms later) rebuilds its in-memory status from the
|
|
10
|
+
* still-stale state.json, derives the "Needs instructions" fallback summary and
|
|
11
|
+
* overwrites the fresher projection that was still in flight.
|
|
12
|
+
*
|
|
13
|
+
* This cache restores read-your-writes for the two fields whose legitimate
|
|
14
|
+
* transitions are "empty → non-empty" and "old non-empty → new non-empty",
|
|
15
|
+
* keyed on `lastAgentActivityAt` as the freshness signal (message_end stamps
|
|
16
|
+
* it with `now`):
|
|
17
|
+
*
|
|
18
|
+
* - `remember` keeps the strictly-freshest projection: an entry whose
|
|
19
|
+
* timestamp is older than the stored one never degrades it (the stale
|
|
20
|
+
* agent_end rebuild must not clobber a newer in-flight message_end value);
|
|
21
|
+
* an older or timestampless projection only fills gaps.
|
|
22
|
+
* - `backfill` adopts BOTH cached fields when the cached timestamp is strictly
|
|
23
|
+
* newer than the rebuilt status's (including a timestampless/legacy
|
|
24
|
+
* rebuild) — the disk snapshot is stale. Otherwise it fills only empty
|
|
25
|
+
* fields: the materialized file stays authoritative when it is newer or
|
|
26
|
+
* equally aged, so a value another writer persisted is never resurrected
|
|
27
|
+
* over by an older cached one.
|
|
28
|
+
*
|
|
29
|
+
* Module-level by necessity: `serviceFor()` creates a new service instance per
|
|
30
|
+
* call (src/index.ts), so a per-instance cache would be discarded between
|
|
31
|
+
* events.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/** @typedef {{ latestAssistantPreview: string, lastAgentActivityAt: number|null }} KnownForegroundFields */
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @returns {{
|
|
38
|
+
* remember: (viewId: string, projection: { latestAssistantPreview?: unknown, lastAgentActivityAt?: unknown }) => void,
|
|
39
|
+
* backfill: (viewId: string, status: { latestAssistantPreview?: unknown, lastAgentActivityAt?: unknown }) => boolean,
|
|
40
|
+
* forget: (viewId: string) => boolean,
|
|
41
|
+
* clear: () => void,
|
|
42
|
+
* size: () => number,
|
|
43
|
+
* }}
|
|
44
|
+
*/
|
|
45
|
+
export function createForegroundPreviewCache() {
|
|
46
|
+
/** @type {Map<string, KnownForegroundFields>} */
|
|
47
|
+
const known = new Map();
|
|
48
|
+
|
|
49
|
+
function remember(viewId, projection) {
|
|
50
|
+
if (!viewId || !projection) return;
|
|
51
|
+
const preview = projection.latestAssistantPreview;
|
|
52
|
+
const activityAt = projection.lastAgentActivityAt;
|
|
53
|
+
const hasPreview = typeof preview === "string" && preview.length > 0;
|
|
54
|
+
const hasActivity = activityAt != null;
|
|
55
|
+
if (!hasPreview && !hasActivity) return;
|
|
56
|
+
const entry = known.get(viewId) ?? { latestAssistantPreview: "", lastAgentActivityAt: null };
|
|
57
|
+
const isNewer = hasActivity && (entry.lastAgentActivityAt == null || activityAt > entry.lastAgentActivityAt);
|
|
58
|
+
if (isNewer) {
|
|
59
|
+
// A strictly fresher projection wins wholesale; an empty preview still
|
|
60
|
+
// never overwrites a known non-empty one.
|
|
61
|
+
if (hasPreview) entry.latestAssistantPreview = preview;
|
|
62
|
+
entry.lastAgentActivityAt = activityAt;
|
|
63
|
+
} else {
|
|
64
|
+
// Older or timestampless: gap-fill only, never degrade the entry.
|
|
65
|
+
if (hasPreview && !entry.latestAssistantPreview) entry.latestAssistantPreview = preview;
|
|
66
|
+
if (hasActivity && entry.lastAgentActivityAt == null) entry.lastAgentActivityAt = activityAt;
|
|
67
|
+
}
|
|
68
|
+
known.set(viewId, entry);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function backfill(viewId, status) {
|
|
72
|
+
if (!viewId || !status) return false;
|
|
73
|
+
const entry = known.get(viewId);
|
|
74
|
+
if (!entry) return false;
|
|
75
|
+
const statusAt = status.lastAgentActivityAt ?? null;
|
|
76
|
+
let changed = false;
|
|
77
|
+
if (entry.lastAgentActivityAt != null && (statusAt == null || entry.lastAgentActivityAt > statusAt)) {
|
|
78
|
+
// The disk rebuild is a stale snapshot (or a timestampless legacy row):
|
|
79
|
+
// the freshest value this process projected wins wholesale.
|
|
80
|
+
if (entry.latestAssistantPreview && status.latestAssistantPreview !== entry.latestAssistantPreview) {
|
|
81
|
+
status.latestAssistantPreview = entry.latestAssistantPreview;
|
|
82
|
+
changed = true;
|
|
83
|
+
}
|
|
84
|
+
if (statusAt !== entry.lastAgentActivityAt) {
|
|
85
|
+
status.lastAgentActivityAt = entry.lastAgentActivityAt;
|
|
86
|
+
changed = true;
|
|
87
|
+
}
|
|
88
|
+
return changed;
|
|
89
|
+
}
|
|
90
|
+
if (entry.latestAssistantPreview && !status.latestAssistantPreview) {
|
|
91
|
+
status.latestAssistantPreview = entry.latestAssistantPreview;
|
|
92
|
+
changed = true;
|
|
93
|
+
}
|
|
94
|
+
if (entry.lastAgentActivityAt != null && statusAt == null) {
|
|
95
|
+
status.lastAgentActivityAt = entry.lastAgentActivityAt;
|
|
96
|
+
changed = true;
|
|
97
|
+
}
|
|
98
|
+
return changed;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function forget(viewId) {
|
|
102
|
+
return known.delete(viewId);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function clear() {
|
|
106
|
+
known.clear();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function size() {
|
|
110
|
+
return known.size;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { remember, backfill, forget, clear, size };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Shared cache used by the runtime service (module-level: see module doc). */
|
|
117
|
+
export const foregroundPreviewCache = createForegroundPreviewCache();
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-local read-your-writes cache for foreground status projections
|
|
3
|
+
* (issue #225).
|
|
4
|
+
*
|
|
5
|
+
* `syncRowEvent` rebuilds its status from `state.json` on every event, while
|
|
6
|
+
* `writeForegroundState` is fire-and-forget: two events can have two
|
|
7
|
+
* `sync_foreground` commands in flight, and an older projection can materialize
|
|
8
|
+
* last. Because the rebuilt status carries no freshness signal, the next event
|
|
9
|
+
* reduces onto the stale state and writes it back — a cleared question comes
|
|
10
|
+
* back as `needs_input` until a terminal event lands.
|
|
11
|
+
*
|
|
12
|
+
* This cache stores the freshest whitelisted status fields with a strictly
|
|
13
|
+
* increasing per-view stamp (`nextStamp`), so a disk rebuild that is older than
|
|
14
|
+
* the last write is corrected before the new event is reduced.
|
|
15
|
+
*
|
|
16
|
+
* Module-level by necessity: `serviceFor()` creates a new service instance per
|
|
17
|
+
* call (src/index.ts), so a per-instance cache would be discarded between
|
|
18
|
+
* events.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** Fields a cached snapshot may restore on a stale rebuild. */
|
|
22
|
+
const CACHED_FIELDS = [
|
|
23
|
+
"semanticState",
|
|
24
|
+
"processState",
|
|
25
|
+
"summary",
|
|
26
|
+
"currentTool",
|
|
27
|
+
"question",
|
|
28
|
+
"pendingQuestions",
|
|
29
|
+
"error",
|
|
30
|
+
"lastActivityAt",
|
|
31
|
+
"latestAssistantPreview",
|
|
32
|
+
"lastAgentActivityAt",
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
/** @param {any} value */
|
|
36
|
+
function cloneValue(value) {
|
|
37
|
+
if (value === null || typeof value !== "object") return value;
|
|
38
|
+
return JSON.parse(JSON.stringify(value));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** @param {any} status */
|
|
42
|
+
function snapshot(status) {
|
|
43
|
+
/** @type {Record<string, unknown>} */
|
|
44
|
+
const fields = {};
|
|
45
|
+
for (const key of CACHED_FIELDS) fields[key] = cloneValue(status?.[key]);
|
|
46
|
+
return fields;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function createForegroundProjectionCache() {
|
|
50
|
+
/** @type {Map<string, { stamp: number, fields: Record<string, unknown> }>} */
|
|
51
|
+
const known = new Map();
|
|
52
|
+
/** @type {Map<string, number>} */
|
|
53
|
+
const stamps = new Map();
|
|
54
|
+
|
|
55
|
+
function nextStamp(viewId) {
|
|
56
|
+
const stamp = Math.max(Date.now(), (stamps.get(viewId) ?? 0) + 1);
|
|
57
|
+
stamps.set(viewId, stamp);
|
|
58
|
+
return stamp;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function remember(viewId, status, stamp) {
|
|
62
|
+
if (!viewId || !status || typeof stamp !== "number" || !Number.isFinite(stamp)) return;
|
|
63
|
+
known.set(viewId, { stamp, fields: snapshot(status) });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function adopt(viewId, status) {
|
|
67
|
+
if (!viewId || !status) return false;
|
|
68
|
+
const entry = known.get(viewId);
|
|
69
|
+
if (!entry) return false;
|
|
70
|
+
const at = typeof status.updatedAt === "number" ? status.updatedAt : 0;
|
|
71
|
+
if (entry.stamp <= at) return false;
|
|
72
|
+
for (const key of CACHED_FIELDS) status[key] = cloneValue(entry.fields[key]);
|
|
73
|
+
status.updatedAt = entry.stamp;
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function forget(viewId) {
|
|
78
|
+
stamps.delete(viewId);
|
|
79
|
+
return known.delete(viewId);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function clear() {
|
|
83
|
+
known.clear();
|
|
84
|
+
stamps.clear();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function size() {
|
|
88
|
+
return known.size;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { nextStamp, remember, adopt, forget, clear, size };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Shared cache used by the runtime service (module-level: see module doc).
|
|
96
|
+
* Keyed by view id only, matching the preview cache: production view ids are generated
|
|
97
|
+
* and never reused, and tests clear the cache per case.
|
|
98
|
+
*/
|
|
99
|
+
export const foregroundProjectionCache = createForegroundProjectionCache();
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coalesce host.json frame-witness summary writes only. S/F/A protocol records
|
|
3
|
+
* stay synchronous; this scheduler controls only the secondary host summary.
|
|
4
|
+
*/
|
|
5
|
+
export function createFrameWitnessSummaryScheduler({
|
|
6
|
+
delayMs = 100,
|
|
7
|
+
publish,
|
|
8
|
+
setTimeoutFn = globalThis.setTimeout,
|
|
9
|
+
clearTimeoutFn = globalThis.clearTimeout,
|
|
10
|
+
}) {
|
|
11
|
+
if (typeof publish !== "function") throw new TypeError("publish is required");
|
|
12
|
+
let timer = null;
|
|
13
|
+
const cancel = () => {
|
|
14
|
+
if (timer === null) return;
|
|
15
|
+
clearTimeoutFn(timer);
|
|
16
|
+
timer = null;
|
|
17
|
+
};
|
|
18
|
+
const flush = () => {
|
|
19
|
+
cancel();
|
|
20
|
+
publish();
|
|
21
|
+
};
|
|
22
|
+
const schedule = () => {
|
|
23
|
+
if (timer !== null) return;
|
|
24
|
+
if (delayMs <= 0) {
|
|
25
|
+
publish();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
timer = setTimeoutFn(() => {
|
|
29
|
+
timer = null;
|
|
30
|
+
publish();
|
|
31
|
+
}, delayMs);
|
|
32
|
+
timer.unref?.();
|
|
33
|
+
};
|
|
34
|
+
return { schedule, flush, cancel };
|
|
35
|
+
}
|