thinkpool-pair 0.7.364 → 0.7.366
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 +18 -0
- package/README.md +20 -0
- package/abort-turn-barrier.mjs +1 -23
- package/account.mjs +1 -1437
- package/acp-client.mjs +1 -140
- package/agent-detect.mjs +1 -28
- package/agent-notify.mjs +1 -142
- package/agent-visibility.mjs +1 -67
- package/auth-store.mjs +1 -120
- package/bridge.mjs +1 -6266
- package/byok-detect.mjs +1 -126
- package/claude-command-catalog.mjs +1 -91
- package/claude-session.mjs +1 -1519
- package/code-event-contract.mjs +1 -118
- package/codex-app-server.mjs +1 -340
- package/codex-commands.mjs +1 -83
- package/codex-event-mapper.mjs +1 -232
- package/codex-images.mjs +1 -69
- package/codex-mcp-http.mjs +1 -131
- package/codex-session.mjs +1 -1295
- package/command-catalog.mjs +1 -118
- package/command-guidance.mjs +1 -8
- package/context-contract.mjs +1 -95
- package/context-windows.mjs +1 -107
- package/cross-terminal.mjs +1 -789
- package/cumulative-event-relay.mjs +1 -53
- package/design-edit.mjs +1 -424
- package/design-source-contract.mjs +1 -4
- package/direct-pair-room.mjs +1 -57
- package/dispatch-lease.mjs +1 -37
- package/dispatch-permission-cleanup.mjs +1 -86
- package/edit-diff.mjs +1 -136
- package/error-recovery.mjs +1 -50
- package/event-bounds.mjs +1 -121
- package/event-delivery-queue.mjs +1 -60
- package/event-id.mjs +1 -549
- package/evidence-citations.mjs +1 -50
- package/evidence-compact.mjs +1 -11
- package/flow-assembly.mjs +1 -196
- package/flow-budget.mjs +1 -84
- package/flow-conductor.mjs +1 -259
- package/flow-context-store.mjs +1 -387
- package/flow-host-revert.mjs +1 -42
- package/flow-models.mjs +1 -139
- package/flow-preview.mjs +1 -148
- package/flow-receipt.mjs +1 -122
- package/flow-redispatch.mjs +1 -72
- package/flow-review-gate.mjs +1 -402
- package/flow-review-reflect.mjs +1 -115
- package/flow-review.mjs +1 -152
- package/flow-scope-evidence.mjs +1 -117
- package/flow-skill-registry.mjs +1 -140
- package/flow-task-graph.mjs +1 -562
- package/flow-worktree.mjs +1 -71
- package/git-diff-report.mjs +1 -121
- package/hermes-delegation-guard.mjs +1 -14
- package/hermes-event-mapper.mjs +1 -194
- package/hermes-isolation.mjs +1 -61
- package/hermes-model-cache.mjs +1 -54
- package/hermes-policy.mjs +1 -92
- package/hermes-probe.mjs +1 -57
- package/hermes-session.mjs +1 -683
- package/hermes-setup.mjs +1 -167
- package/host-memory.mjs +1 -116
- package/interrupted-resume.mjs +1 -95
- package/keep-awake.mjs +1 -148
- package/key-shape.mjs +1 -49
- package/lane-continuation.mjs +1 -83
- package/lane-lifecycle.mjs +1 -189
- package/lane-worktree.mjs +1 -77
- package/launcher.mjs +1 -354
- package/mcp-flight-recorder.mjs +1 -79
- package/mockup-delivery.mjs +1 -57
- package/model-prices.mjs +1 -113
- package/package.json +13 -4
- package/pair-bus.mjs +1 -98
- package/pair-control-authority.mjs +1 -89
- package/past-work-search.mjs +1 -105
- package/plan-meters.mjs +1 -144
- package/presence.mjs +1 -191
- package/privacy-report.mjs +1 -108
- package/provider-resilience.mjs +1 -356
- package/provider.mjs +1 -133
- package/providers.mjs +1 -491
- package/publish-guard.mjs +2 -0
- package/publish-manifest.json +129 -0
- package/question-response.mjs +1 -58
- package/reap-terminal.mjs +1 -68
- package/recap.mjs +1 -297
- package/replay-transport.mjs +1 -64
- package/repo-search.mjs +1 -190
- package/review-check.mjs +1 -182
- package/runtime-contract.mjs +1 -93
- package/runtime-registry.mjs +1 -64
- package/runtime-session.mjs +1 -20
- package/scheduled-run-admission.mjs +1 -364
- package/scheduled-runs.mjs +1 -268
- package/sdk-admission.mjs +1 -9
- package/sdk-smoke.mjs +1 -61
- package/serve-consent.mjs +1 -118
- package/serve-dir.mjs +1 -40
- package/service.mjs +1 -877
- package/session-store.mjs +1 -484
- package/side-lane.mjs +1 -63
- package/supabase-key.mjs +1 -176
- package/supervisor-ready.mjs +1 -57
- package/switch-provider.mjs +1 -84
- package/terminal-name.mjs +1 -359
- package/terminal-row-reconcile.mjs +1 -70
- package/thinkpool-prompt-contracts.mjs +1 -85
- package/thinkpool-room-prompt.mjs +1 -187
- package/transcript-sanitize.mjs +1 -332
- package/turn-stall.mjs +1 -61
- package/update-gate.mjs +1 -53
- package/viewport.mjs +1 -810
- package/worker-completion.mjs +1 -57
package/question-response.mjs
CHANGED
|
@@ -1,58 +1 @@
|
|
|
1
|
-
function
|
|
2
|
-
if (typeof decision !== 'string' || !decision.startsWith('answer:')) return {}
|
|
3
|
-
const text = decision.slice(7).trim()
|
|
4
|
-
if (!text || !questions.length) return {}
|
|
5
|
-
if (questions.length === 1) {
|
|
6
|
-
const question = questions[0]
|
|
7
|
-
const prefix = `${question.header || 'Q1'}: `
|
|
8
|
-
const value = text.startsWith(prefix) ? text.slice(prefix.length) : text
|
|
9
|
-
return { [String(question.id || 0)]: { answers: [value] } }
|
|
10
|
-
}
|
|
11
|
-
const answers = {}
|
|
12
|
-
questions.forEach((question, index) => {
|
|
13
|
-
const id = String(question.id || index)
|
|
14
|
-
const marker = `${index ? '; ' : ''}${question.header || `Q${index + 1}`}: `
|
|
15
|
-
const start = text.indexOf(marker)
|
|
16
|
-
if (start < 0) return
|
|
17
|
-
const valueStart = start + marker.length
|
|
18
|
-
const nextQuestion = questions[index + 1]
|
|
19
|
-
const nextMarker = nextQuestion ? `; ${nextQuestion.header || `Q${index + 2}`}: ` : null
|
|
20
|
-
const end = nextMarker ? text.indexOf(nextMarker, valueStart) : -1
|
|
21
|
-
const value = text.slice(valueStart, end < 0 ? undefined : end).trim()
|
|
22
|
-
if (value) answers[id] = { answers: [value] }
|
|
23
|
-
})
|
|
24
|
-
return answers
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Codex and Hermes use different native tool protocols, but the room answer is
|
|
28
|
-
// one contract. Keep filtering, multi-select preservation, and legacy fallback
|
|
29
|
-
// in one pure adapter so the two runtimes cannot drift independently.
|
|
30
|
-
export function questionAnswerResponse (result, questions = []) {
|
|
31
|
-
const supplied = result && typeof result === 'object' ? result.answers : null
|
|
32
|
-
const answers = {}
|
|
33
|
-
if (supplied && typeof supplied === 'object') {
|
|
34
|
-
const allowed = new Set(questions.map((question, index) => String(question?.id || index)))
|
|
35
|
-
for (const [id, value] of Object.entries(supplied)) {
|
|
36
|
-
if (!allowed.has(String(id)) || !Array.isArray(value?.answers)) continue
|
|
37
|
-
const values = value.answers.map((answer) => String(answer)).filter(Boolean)
|
|
38
|
-
if (values.length) answers[String(id)] = { answers: values }
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
const responder = result?.responder && typeof result.responder === 'object'
|
|
42
|
-
? {
|
|
43
|
-
id: typeof result.responder.id === 'string' ? result.responder.id : null,
|
|
44
|
-
name: typeof result.responder.name === 'string' ? result.responder.name : null,
|
|
45
|
-
}
|
|
46
|
-
: null
|
|
47
|
-
if (Object.keys(answers).length) return { answers, ...(responder ? { responder } : {}) }
|
|
48
|
-
const decision = result && typeof result === 'object' ? result.decision : result
|
|
49
|
-
return { answers: legacyQuestionAnswers(decision, questions), ...(responder ? { responder } : {}) }
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function hermesUserInputResponse (result, questions = []) {
|
|
53
|
-
const decision = result && typeof result === 'object' ? result.decision : result
|
|
54
|
-
return {
|
|
55
|
-
status: String(decision || '').startsWith('answer:') ? 'answered' : decision === 'timeout' ? 'timed_out' : 'dismissed',
|
|
56
|
-
...questionAnswerResponse(result, questions),
|
|
57
|
-
}
|
|
58
|
-
}
|
|
1
|
+
function e(e,n){if("string"!=typeof e||!e.startsWith("answer:"))return{};const t=e.slice(7).trim();if(!t||!n.length)return{};if(1===n.length){const e=n[0],r=`${e.header||"Q1"}: `,s=t.startsWith(r)?t.slice(r.length):t;return{[String(e.id||0)]:{answers:[s]}}}const r={};return n.forEach((e,s)=>{const o=String(e.id||s),i=`${s?"; ":""}${e.header||`Q${s+1}`}: `,a=t.indexOf(i);if(a<0)return;const c=a+i.length,d=n[s+1],p=d?`; ${d.header||`Q${s+2}`}: `:null,f=p?t.indexOf(p,c):-1,u=t.slice(c,f<0?void 0:f).trim();u&&(r[o]={answers:[u]})}),r}export function questionAnswerResponse(n,t=[]){const r=n&&"object"==typeof n?n.answers:null,s={};if(r&&"object"==typeof r){const e=new Set(t.map((e,n)=>String(e?.id||n)));for(const[n,t]of Object.entries(r)){if(!e.has(String(n))||!Array.isArray(t?.answers))continue;const r=t.answers.map(e=>String(e)).filter(Boolean);r.length&&(s[String(n)]={answers:r})}}const o=n?.responder&&"object"==typeof n.responder?{id:"string"==typeof n.responder.id?n.responder.id:null,name:"string"==typeof n.responder.name?n.responder.name:null}:null;return Object.keys(s).length?{answers:s,...o?{responder:o}:{}}:{answers:e(n&&"object"==typeof n?n.decision:n,t),...o?{responder:o}:{}}}export function hermesUserInputResponse(e,n=[]){const t=e&&"object"==typeof e?e.decision:e;return{status:String(t||"").startsWith("answer:")?"answered":"timeout"===t?"timed_out":"dismissed",...questionAnswerResponse(e,n)}}
|
package/reap-terminal.mjs
CHANGED
|
@@ -1,68 +1 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
// Spec: docs/specs/2026-07-04-structured-terminal-durability.md §Bridge-side reap.
|
|
4
|
-
//
|
|
5
|
-
// WHY this exists: the row-reap on a clean structured close (endStructured / MCP
|
|
6
|
-
// close_terminal) used to be CLIENT-executed only — it ran in whichever browser tab
|
|
7
|
-
// caught the transient `term-exit` broadcast (room.jsx #205). Close a lane while no fresh
|
|
8
|
-
// tab is listening (stale / asleep) → the broadcast evaporates → a permanent dormant ghost
|
|
9
|
-
// row (three hand-deleted 2026-07-04/05). The bridge is the ONE authoritative witness of a
|
|
10
|
-
// clean structured close, so it reaps the row ITSELF — the client reap is kept as the
|
|
11
|
-
// fallback for old bridges and for the fast-tab case.
|
|
12
|
-
//
|
|
13
|
-
// Extracted from bridge.mjs (which has top-level side effects and can't be unit-imported)
|
|
14
|
-
// so the reap DECISION + REST shape are testable with an injected fetch — same pattern as
|
|
15
|
-
// serve-consent.mjs. bridge.mjs wraps this, closing over its live codeAuthToken /
|
|
16
|
-
// shuttingDown state.
|
|
17
|
-
//
|
|
18
|
-
// RLS: `code_terminals_member_delete` permits DELETE to any member of the parent code
|
|
19
|
-
// session; the bridge serves as the room owner or an authed grantee (codeAuthToken), so it
|
|
20
|
-
// qualifies. Anon bridges (no token) skip — the client reap covers them.
|
|
21
|
-
//
|
|
22
|
-
// Scope guard: NEVER reap while shuttingDown — a late cleanupFlowLanes timer firing during
|
|
23
|
-
// teardown / stand-down / restart must not delete rows. Dormant lanes across a restart are
|
|
24
|
-
// the feature, not the leak.
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* @returns {Promise<{reaped: boolean, reason: 'ok'|'shutdown'|'anon'|'no-id'|'error', error?: string, status?: number}>}
|
|
28
|
-
* Never throws — best-effort; the client reap (#205) is the fallback.
|
|
29
|
-
*/
|
|
30
|
-
export async function reapTerminalRow ({ id, token, supabaseUrl, anonKey, shuttingDown = false, fetchImpl = fetch, onFailure } = {}) {
|
|
31
|
-
const fail = (outcome) => {
|
|
32
|
-
try { onFailure?.(outcome) } catch { /* observability must not break a close */ }
|
|
33
|
-
return outcome
|
|
34
|
-
}
|
|
35
|
-
if (shuttingDown) return { reaped: false, reason: 'shutdown' }
|
|
36
|
-
if (!token) return { reaped: false, reason: 'anon' }
|
|
37
|
-
if (!id) return { reaped: false, reason: 'no-id' }
|
|
38
|
-
try {
|
|
39
|
-
// A terminal at the 50k transcript cap cannot archive + cascade-delete inside
|
|
40
|
-
// the authenticated role's ordinary 8s statement timeout. The member-authorized
|
|
41
|
-
// RPC scopes a longer timeout to this one cleanup operation; a raw REST DELETE
|
|
42
|
-
// timed out and left the row as a cross-device dormant ghost (8TMX546Q).
|
|
43
|
-
const response = await fetchImpl(`${supabaseUrl}/rest/v1/rpc/delete_code_terminal`, {
|
|
44
|
-
method: 'POST',
|
|
45
|
-
headers: {
|
|
46
|
-
apikey: anonKey,
|
|
47
|
-
Authorization: `Bearer ${token}`,
|
|
48
|
-
'Content-Type': 'application/json',
|
|
49
|
-
},
|
|
50
|
-
body: JSON.stringify({ p_terminal: id }),
|
|
51
|
-
})
|
|
52
|
-
// fetch resolves for HTTP failures. A 401/500 did not reap the row and must
|
|
53
|
-
// stay visible to the fire-and-forget caller as a failed best-effort cleanup.
|
|
54
|
-
if (!response?.ok) {
|
|
55
|
-
let detail = ''
|
|
56
|
-
try { detail = String(await response.text()).slice(0, 240) } catch { /* no body */ }
|
|
57
|
-
return fail({
|
|
58
|
-
reaped: false,
|
|
59
|
-
reason: 'error',
|
|
60
|
-
error: `HTTP ${response?.status ?? 'unknown'}${response?.statusText ? ` ${response.statusText}` : ''}${detail ? `: ${detail}` : ''}`,
|
|
61
|
-
status: response?.status,
|
|
62
|
-
})
|
|
63
|
-
}
|
|
64
|
-
return { reaped: true, reason: 'ok' }
|
|
65
|
-
} catch (e) {
|
|
66
|
-
return fail({ reaped: false, reason: 'error', error: e?.message })
|
|
67
|
-
}
|
|
68
|
-
}
|
|
1
|
+
export async function reapTerminalRow({id:e,token:r,supabaseUrl:t,anonKey:a,shuttingDown:n=!1,fetchImpl:o=fetch,onFailure:s}={}){const i=e=>{try{s?.(e)}catch{}return e};if(n)return{reaped:!1,reason:"shutdown"};if(!r)return{reaped:!1,reason:"anon"};if(!e)return{reaped:!1,reason:"no-id"};try{const n=await o(`${t}/rest/v1/rpc/delete_code_terminal`,{method:"POST",headers:{apikey:a,Authorization:`Bearer ${r}`,"Content-Type":"application/json"},body:JSON.stringify({p_terminal:e})});if(!n?.ok){let e="";try{e=String(await n.text()).slice(0,240)}catch{}return i({reaped:!1,reason:"error",error:`HTTP ${n?.status??"unknown"}${n?.statusText?` ${n.statusText}`:""}${e?`: ${e}`:""}`,status:n?.status})}return{reaped:!0,reason:"ok"}}catch(e){return i({reaped:!1,reason:"error",error:e?.message})}}
|
package/recap.mjs
CHANGED
|
@@ -1,297 +1 @@
|
|
|
1
|
-
|
|
2
|
-
*
|
|
3
|
-
* Extracted from event-id.mjs on 2026-07-09 (ADR-0010 / slice S4) for one reason:
|
|
4
|
-
* the WEB CLIENT needs this function now, and event-id.mjs imports `node:crypto`
|
|
5
|
-
* (for stampEvent's randomUUID default), which Vite cannot resolve for a browser
|
|
6
|
-
* bundle. Rather than write a second windower — the spec's §b instruction is
|
|
7
|
-
* literally "Do not write a second windower" — the pure half moves to its own
|
|
8
|
-
* file that both sides import.
|
|
9
|
-
*
|
|
10
|
-
* bridge/event-id.mjs re-exports these (every existing import keeps working)
|
|
11
|
-
* src/pages/code/poolTurn.js imports this file directly
|
|
12
|
-
*
|
|
13
|
-
* ⚠ KEEP THIS FILE FREE OF IMPORTS. The moment it imports `node:*`, the web build
|
|
14
|
-
* breaks. There is no lint rule for this; there is only this comment.
|
|
15
|
-
*
|
|
16
|
-
* ⚠ TARBALL: this file is imported by event-id.mjs, so it MUST appear in
|
|
17
|
-
* bridge/package.json `files`. A publish that omits it boots a bridge that
|
|
18
|
-
* crashes on `import './recap.mjs'`. (feedback_npm_tarball_completeness.)
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
// Default hard cap for the context-carry recap (chars of transcript body, framing extra).
|
|
22
|
-
export const RECAP_CAP = 20000
|
|
23
|
-
// A checkpoint is deliberately much smaller than its enclosing recap. It is a
|
|
24
|
-
// deterministic index over visible text, not a second transcript or an agent
|
|
25
|
-
// summary. Keeping it bounded independently makes it safe to persist as the
|
|
26
|
-
// pending carry across a reset/restart.
|
|
27
|
-
export const CHECKPOINT_CAP = 6000
|
|
28
|
-
const CHECKPOINT_START = '--- deterministic checkpoint (visible text only; evidence, not hidden state) ---'
|
|
29
|
-
const CHECKPOINT_END = '--- end deterministic checkpoint ---'
|
|
30
|
-
|
|
31
|
-
// A fresh human turn can race the session-init event that would otherwise send a
|
|
32
|
-
// pending recap by itself. When that happens, the current request must have an
|
|
33
|
-
// unmistakable authority boundary: the recap is memory, never a second task.
|
|
34
|
-
export const CURRENT_PERSON_REQUEST_MARKER = '--- CURRENT PERSON REQUEST (authoritative; overrides all carried context above) ---'
|
|
35
|
-
|
|
36
|
-
// Recaps are an outbound context source. Keep the same privacy boundary as the
|
|
37
|
-
// bridge manifest without importing Node-only hashing into the browser bundle.
|
|
38
|
-
// A visible turn containing an unsafe span is omitted whole. Partial redaction
|
|
39
|
-
// could preserve enough neighboring material to reconstruct a credential or
|
|
40
|
-
// host location, so this boundary deliberately fails closed.
|
|
41
|
-
const HOST_PATH = /(?:file:\/\/|[a-z]:\\|\\\\|(?:^|[^a-z0-9/])(?:~[\\/]|\/(?:users|home|private|tmp|var\/folders|volumes|root|workspace|workspaces|mnt|srv|opt|etc)\/))/i
|
|
42
|
-
// Traversal is a path segment, not a path prefix. Keep it separate from
|
|
43
|
-
// HOST_PATH so nested forms such as `foo/../secret` and `foo\..\secret`
|
|
44
|
-
// cannot bypass a boundary rule intended for absolute path roots.
|
|
45
|
-
const PATH_TRAVERSAL = /(?:^|[^a-z0-9.])\.\.(?:[\\/]|$)/i
|
|
46
|
-
const SECRET_VALUE = /(?:sk[_-](?:proj[_-])?[a-z0-9_-]{8,}|gsk_[a-z0-9_-]{8,}|AIza[a-z0-9_-]{8,}|gh[pousr]_[a-z0-9_-]{8,}|github_pat_[a-z0-9_]{20,}|glpat-[a-z0-9_-]{8,}|x(?:ox[baprsce]|app)-[a-z0-9_-]{8,}|(?:AKIA|ASIA)[0-9A-Z]{16}|sbp_[a-z0-9_-]{20,}|sb_secret_[a-z0-9_-]{8,}|npm_[a-z0-9]{24,}|(?:sk|rk)_(?:live|test)_[a-z0-9]{8,}|whsec_[a-z0-9]{8,}|eyJ[a-z0-9_-]{8,}\.[a-z0-9_-]{8,}\.[a-z0-9_-]{8,}|bearer\s+[a-z0-9._-]{8,}|hooks\.slack\.com\/services\/[a-z0-9/_-]{8,})/i
|
|
47
|
-
const SECRET_ASSIGNMENT = /(?:[a-z0-9]+[_ -])*(?:token|secret(?:[_ -]access[_ -]key)?|password|authorization|api[_ -]?key|private[_ -]?key|access[_ -]?key(?:[_ -]?id)?)\s*[:=]\s*\S{4,}/i
|
|
48
|
-
const PROHIBITED_CONTEXT = /(?:BEGIN (?:RSA |OPENSSH )?PRIVATE KEY|raw transcript|tool args?|chain[ -]of[ -]thought|hidden reasoning|system prompt|environment dump|provider key)/i
|
|
49
|
-
const safeRecapText = (value) => {
|
|
50
|
-
const text = String(value || '').trim()
|
|
51
|
-
return text && !HOST_PATH.test(text) && !PATH_TRAVERSAL.test(text) && !SECRET_VALUE.test(text) && !SECRET_ASSIGNMENT.test(text) && !PROHIBITED_CONTEXT.test(text) ? text : ''
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const clip = (value, cap) => {
|
|
55
|
-
const text = String(value || '').trim().replace(/\s+/g, ' ')
|
|
56
|
-
if (!text) return ''
|
|
57
|
-
return text.length <= cap ? text : `${text.slice(0, Math.max(0, cap - 1)).trimEnd()}…`
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const visibleTurns = (log) => {
|
|
61
|
-
if (!Array.isArray(log)) return []
|
|
62
|
-
const turns = []
|
|
63
|
-
for (const event of log) {
|
|
64
|
-
if (!event || typeof event !== 'object') continue
|
|
65
|
-
if (event.kind === 'you') {
|
|
66
|
-
const text = safeRecapText(event.text)
|
|
67
|
-
if (text) turns.push({ who: 'person', text })
|
|
68
|
-
continue
|
|
69
|
-
}
|
|
70
|
-
if (event.kind !== 'assistant') continue
|
|
71
|
-
const text = (Array.isArray(event.blocks) ? event.blocks : [])
|
|
72
|
-
.filter((block) => block?.type === 'text')
|
|
73
|
-
.map((block) => safeRecapText(block.text))
|
|
74
|
-
.filter(Boolean)
|
|
75
|
-
.join('\n')
|
|
76
|
-
if (text) turns.push({ who: 'assistant', text })
|
|
77
|
-
}
|
|
78
|
-
return turns
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const unique = (values, cap = 6) => [...new Set(values.filter(Boolean))].slice(0, cap)
|
|
82
|
-
const sentenceCandidates = (turns, matcher, cap = 4) => unique(
|
|
83
|
-
turns.flatMap((turn) => turn.text.split(/(?<=[.!?])\s+|\n+/)
|
|
84
|
-
.map((line) => clip(line, 500))
|
|
85
|
-
.filter((line) => matcher.test(line))),
|
|
86
|
-
cap,
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
const publicReceipts = (turns) => {
|
|
90
|
-
const receipts = []
|
|
91
|
-
for (const { text } of turns) {
|
|
92
|
-
// Never infer a receipt from an unsafe source turn; safeRecapText already
|
|
93
|
-
// rejected host paths, credential-shaped values, and hidden/tool text.
|
|
94
|
-
for (const sha of text.matchAll(/\b[0-9a-f]{7,40}\b/gi)) receipts.push(`commit ${sha[0]}`)
|
|
95
|
-
for (const file of text.matchAll(/(?<![\w/])(?:[\w.-]+\/)+[\w.-]+\.(?:[cm]?[jt]sx?|json|md|css|html|py|sh|yml|yaml)\b/g)) receipts.push(`file ${file[0]}`)
|
|
96
|
-
for (const url of text.matchAll(/https?:\/\/[^\s)\]}>]+/gi)) {
|
|
97
|
-
const candidate = safeRecapText(url[0])
|
|
98
|
-
if (candidate) receipts.push(`URL ${candidate}`)
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
return unique(receipts, 8)
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/*
|
|
105
|
-
* Build a small, explicit resume checkpoint from *only* the visible person and
|
|
106
|
-
* assistant text that buildRecapFromLog is allowed to carry. This intentionally
|
|
107
|
-
* does not read tool calls/results, thinking, runtime state, files, or external
|
|
108
|
-
* systems. It is heuristic evidence, so labels stay conservative: absence is
|
|
109
|
-
* stated as "not established" rather than invented work/completion.
|
|
110
|
-
*/
|
|
111
|
-
export function buildCheckpointFromLog(log, cap = CHECKPOINT_CAP) {
|
|
112
|
-
const budget = Math.max(0, Number.isFinite(cap) ? Math.floor(cap) : 0)
|
|
113
|
-
if (!budget) return ''
|
|
114
|
-
const turns = visibleTurns(log)
|
|
115
|
-
if (!turns.length) return ''
|
|
116
|
-
const people = turns.filter((turn) => turn.who === 'person')
|
|
117
|
-
const assistants = turns.filter((turn) => turn.who === 'assistant')
|
|
118
|
-
const objective = clip(people.at(-1)?.text || 'not established from visible text', 1400)
|
|
119
|
-
const recent = clip(assistants.at(-1)?.text || turns.at(-1)?.text || 'not established from visible text', 1400)
|
|
120
|
-
const blockers = sentenceCandidates(turns, /\b(block(?:ed|er|ing)?|cannot|can['’]?t|unable|waiting|stuck|failed|failure|error|unverified)\b/i)
|
|
121
|
-
const attempted = sentenceCandidates(assistants, /\b(implemented|added|changed|updated|fixed|ran|tested|checked|inspected|tried|attempted|committed|built)\b/i)
|
|
122
|
-
const completed = sentenceCandidates(assistants, /\b(done|complete(?:d)?|implemented|fixed|passing|passed|succeeded|shipped)\b/i)
|
|
123
|
-
const next = clip(
|
|
124
|
-
people.length > 1 ? people.at(-1)?.text :
|
|
125
|
-
sentenceCandidates(assistants, /\b(next|remaining|follow[- ]?up|todo|will)\b/i, 1)[0] || 'not established from visible text',
|
|
126
|
-
1000,
|
|
127
|
-
)
|
|
128
|
-
const lines = [
|
|
129
|
-
CHECKPOINT_START,
|
|
130
|
-
`Objective / live request: ${objective}`,
|
|
131
|
-
`Current step / recent work: ${recent}`,
|
|
132
|
-
`Blockers: ${blockers.length ? blockers.join(' | ') : 'none stated in visible text'}`,
|
|
133
|
-
`Attempted / completed actions: ${attempted.length || completed.length ? unique([...attempted, ...completed], 6).join(' | ') : 'not established from visible text'}`,
|
|
134
|
-
`Receipts: ${publicReceipts(turns).join(' | ') || 'none safely detected'}`,
|
|
135
|
-
`Next action: ${next}`,
|
|
136
|
-
CHECKPOINT_END,
|
|
137
|
-
]
|
|
138
|
-
// Preserve explicit labels under a small caller cap; trim values before
|
|
139
|
-
// trimming structure so a checkpoint never becomes a misleading fragment.
|
|
140
|
-
let checkpoint = lines.join('\n')
|
|
141
|
-
if (checkpoint.length <= budget) return checkpoint
|
|
142
|
-
const minimal = [
|
|
143
|
-
lines[0],
|
|
144
|
-
'Objective / live request:',
|
|
145
|
-
'Current step / recent work:',
|
|
146
|
-
'Blockers:',
|
|
147
|
-
'Attempted / completed actions:',
|
|
148
|
-
'Receipts:',
|
|
149
|
-
'Next action:',
|
|
150
|
-
lines.at(-1),
|
|
151
|
-
].join('\n')
|
|
152
|
-
// A partial envelope could be mistaken for a complete checkpoint. Fail closed
|
|
153
|
-
// rather than omit a label or expose an over-budget payload.
|
|
154
|
-
if (minimal.length > budget) return ''
|
|
155
|
-
const allowance = budget - minimal.length - 6 // one separating space per field
|
|
156
|
-
const values = [objective, recent, blockers.join(' | '), unique([...attempted, ...completed], 6).join(' | '), publicReceipts(turns).join(' | '), next]
|
|
157
|
-
const shares = values.map((value) => clip(value, Math.max(0, Math.floor(allowance / values.length))))
|
|
158
|
-
checkpoint = [
|
|
159
|
-
lines[0],
|
|
160
|
-
`Objective / live request: ${shares[0] || 'not established'}`,
|
|
161
|
-
`Current step / recent work: ${shares[1] || 'not established'}`,
|
|
162
|
-
`Blockers: ${shares[2] || 'none stated'}`,
|
|
163
|
-
`Attempted / completed actions: ${shares[3] || 'not established'}`,
|
|
164
|
-
`Receipts: ${shares[4] || 'none safely detected'}`,
|
|
165
|
-
`Next action: ${shares[5] || 'not established'}`,
|
|
166
|
-
lines.at(-1),
|
|
167
|
-
].join('\n')
|
|
168
|
-
return checkpoint
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
export function recapHasCheckpoint(value) {
|
|
172
|
-
const text = String(value || '')
|
|
173
|
-
return text.includes(CHECKPOINT_START) && text.includes(CHECKPOINT_END)
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// A recap that already contains the checkpoint is the sole delivery owner.
|
|
177
|
-
// Resolve this in one place for both restored sessions and the live fallback
|
|
178
|
-
// compaction path; otherwise an older separately-persisted checkpoint can sit
|
|
179
|
-
// beside the recap and be prepended a second time on the next person turn.
|
|
180
|
-
export function resolveCheckpointCarry(carryRecap, carryCheckpoint) {
|
|
181
|
-
const pendingRecap = typeof carryRecap === 'string' && carryRecap.trim()
|
|
182
|
-
? carryRecap
|
|
183
|
-
: null
|
|
184
|
-
const pendingCheckpoint = !recapHasCheckpoint(pendingRecap) && typeof carryCheckpoint === 'string' && carryCheckpoint.trim()
|
|
185
|
-
? carryCheckpoint
|
|
186
|
-
: null
|
|
187
|
-
return { pendingRecap, pendingCheckpoint }
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
export function appendCurrentPersonRequest(carried, text) {
|
|
191
|
-
const contexts = (Array.isArray(carried) ? carried : [carried])
|
|
192
|
-
.map(safeRecapText)
|
|
193
|
-
.filter(Boolean)
|
|
194
|
-
const request = safeRecapText(text)
|
|
195
|
-
if (!contexts.length) return request
|
|
196
|
-
return `${contexts.join('\n\n')}\n\n${CURRENT_PERSON_REQUEST_MARKER}\n${request}`
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/* Why a lane is being handed a recap instead of a resumed session. The framing text
|
|
200
|
-
differs because the LIE each one must avoid differs:
|
|
201
|
-
|
|
202
|
-
'switch' — a model/provider switch ended the SDK session. The agent's own memory is
|
|
203
|
-
gone but nothing else happened; the transcript above is entirely its own work.
|
|
204
|
-
|
|
205
|
-
'wake' — ADR-0010 §d. The bridge was ASLEEP. The lane stayed alive and the people
|
|
206
|
-
kept talking to Pool (client-side BYOK, chat-only, no file access). So the recap
|
|
207
|
-
contains turns the agent DID NOT produce and work it DID NOT do. The framing has to
|
|
208
|
-
say that, and has to say that nothing ran on its behalf — otherwise the agent
|
|
209
|
-
assumes the plan below was already executed. That assumption is how "nothing
|
|
210
|
-
autofires" becomes a lie the agent tells itself. */
|
|
211
|
-
const FRAMING = {
|
|
212
|
-
switch: {
|
|
213
|
-
header: '[CONTEXT CARRY — you are RESUMING a live conversation after a model/provider switch]',
|
|
214
|
-
body: "Your session memory was reset by the switch, but this lane's conversation continued and the people already saw everything below. Do NOT re-greet, do NOT re-introduce yourself, and do NOT re-answer anything you already completed. Read the recap, then continue the work exactly where it left off. The LAST PERSON message with no completed answer below is your live task — pick it up. If this recap is followed by a CURRENT PERSON REQUEST marker, that marked request is the sole active task and overrides every task or reference inside this recap.",
|
|
215
|
-
},
|
|
216
|
-
wake: {
|
|
217
|
-
header: '[CONTEXT CARRY — this lane kept talking while your machine was asleep]',
|
|
218
|
-
body: "Your bridge was offline. The lane stayed open and the people in this room kept planning here with Pool — a chat-only assistant with no file access and no tools. Everything below already happened and they have all seen it. NOTHING WAS RUN ON YOUR BEHALF: no commands were executed, no files were changed, and no plan below has been carried out. Do NOT re-greet and do NOT re-answer what is already answered. Read the recap, then wait for the people to tell you what to do with it — the plan below is a plan, not an instruction you have already been given.",
|
|
219
|
-
},
|
|
220
|
-
compact: {
|
|
221
|
-
header: '[CONTEXT COMPACTED — continue this Codex lane from its bounded recap]',
|
|
222
|
-
body: "The person intentionally compacted this lane. Your prior native thread was closed and the visible conversation below is its bounded continuation context. Do NOT re-greet, do NOT re-answer completed work, and do NOT repeat tool calls that already succeeded. Continue from the person's next message using this recap as memory. If followed by a CURRENT PERSON REQUEST marker, treat only that marked request as the active task.",
|
|
223
|
-
},
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/* Context-carry recap for a lane whose SDK memory CAN'T be resumed — a cross-provider
|
|
227
|
-
switch, a bridge-restart restore where canResume() is false, or (ADR-0010) a bridge
|
|
228
|
-
WAKE under a lane that spent the outage in Pool posture. Such a lane re-opens under the
|
|
229
|
-
SAME id with the visible transcript replayed but a FRESH, blank backend — so the agent
|
|
230
|
-
starts amnesiac mid-conversation (the 2026-07-08 lane 109d22e1 "this is the first
|
|
231
|
-
message in my lane" incident). Raw SDK resume can't cross a provider (signed thinking
|
|
232
|
-
blocks + provider-specific tool_use records get rejected by a foreign backend), so we
|
|
233
|
-
carry the conversation at the TEXT layer instead: synthesize a plain-text recap from the
|
|
234
|
-
saved log — the people's turns (`you`) + the assistant's own text replies (`assistant`
|
|
235
|
-
text blocks; thinking + tool_use are dropped, they don't survive a backend hop and are
|
|
236
|
-
noise) — newest-weighted under a hard char cap, framed so the agent RESUMES rather than
|
|
237
|
-
re-greets. Pure + import-safe (unit-tested in bridge/recap.test.mjs).
|
|
238
|
-
|
|
239
|
-
capReplayEvents lesson (rooms died once on a multi-MB replay): the char cap keeps this
|
|
240
|
-
bounded in prose form too — an unbounded recap would be the same bug wearing a text hat.
|
|
241
|
-
|
|
242
|
-
`reason` selects the framing only; the windowing is identical and the default
|
|
243
|
-
('switch') is byte-for-byte what this function returned before the S4 extraction. */
|
|
244
|
-
export function buildRecapFromLog(log, cap = RECAP_CAP, { reason = 'switch', includeCheckpoint = true } = {}) {
|
|
245
|
-
if (!Array.isArray(log) || !log.length) return ''
|
|
246
|
-
const budget = Math.max(0, Number.isFinite(cap) ? Math.floor(cap) : 0)
|
|
247
|
-
if (!budget) return ''
|
|
248
|
-
// Flatten the log to speaker-labelled turns, in order. `you` = a person in the room;
|
|
249
|
-
// `assistant` = this agent's own earlier reply (its text blocks only).
|
|
250
|
-
const turns = []
|
|
251
|
-
for (const e of log) {
|
|
252
|
-
if (!e || typeof e !== 'object') continue
|
|
253
|
-
if (e.kind === 'you') {
|
|
254
|
-
const t = safeRecapText(e.text)
|
|
255
|
-
if (t) turns.push({ who: 'PERSON', text: t })
|
|
256
|
-
} else if (e.kind === 'assistant') {
|
|
257
|
-
const t = (Array.isArray(e.blocks) ? e.blocks : [])
|
|
258
|
-
.filter((b) => b && b.type === 'text' && safeRecapText(b.text))
|
|
259
|
-
.map((b) => safeRecapText(b.text))
|
|
260
|
-
.join('\n')
|
|
261
|
-
if (t) turns.push({ who: 'YOU', text: t })
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
if (!turns.length) return ''
|
|
265
|
-
// Newest-weighted: walk from the end, keep whole turns until the char budget is spent,
|
|
266
|
-
// then restore chronological order. A single newest turn bigger than the whole budget is
|
|
267
|
-
// still kept (truncated) so the LIVE task is never dropped to the cap.
|
|
268
|
-
const kept = []
|
|
269
|
-
let used = 0
|
|
270
|
-
let omitted = false
|
|
271
|
-
for (let i = turns.length - 1; i >= 0; i--) {
|
|
272
|
-
let line = `${turns[i].who}: ${turns[i].text}`
|
|
273
|
-
if (used + line.length > budget) {
|
|
274
|
-
if (kept.length) { omitted = true; break } // older turns exist but won't fit → note + stop
|
|
275
|
-
line = line.slice(0, budget) // newest turn alone overflows → clip, still include
|
|
276
|
-
}
|
|
277
|
-
kept.push(line)
|
|
278
|
-
used += line.length
|
|
279
|
-
}
|
|
280
|
-
kept.reverse()
|
|
281
|
-
const body = (omitted ? ['[…earlier messages omitted for length…]'] : []).concat(kept).join('\n\n')
|
|
282
|
-
const frame = FRAMING[reason] || FRAMING.switch
|
|
283
|
-
// Side-lane snapshots already have their own explicit handoff contract. A
|
|
284
|
-
// deterministic resume checkpoint belongs only to a lane that is actually
|
|
285
|
-
// losing SDK context (switch, wake, or compaction); adding it to side
|
|
286
|
-
// snapshots duplicates visible text and can exceed their bounded payload.
|
|
287
|
-
const checkpoint = includeCheckpoint ? buildCheckpointFromLog(log) : ''
|
|
288
|
-
return [
|
|
289
|
-
frame.header,
|
|
290
|
-
frame.body,
|
|
291
|
-
'',
|
|
292
|
-
'--- conversation so far (oldest → newest) ---',
|
|
293
|
-
body,
|
|
294
|
-
'--- end recap ---',
|
|
295
|
-
checkpoint,
|
|
296
|
-
].join('\n')
|
|
297
|
-
}
|
|
1
|
+
export const RECAP_CAP=2e4;export const CHECKPOINT_CAP=6e3;const e="--- deterministic checkpoint (visible text only; evidence, not hidden state) ---",t="--- end deterministic checkpoint ---";export const CURRENT_PERSON_REQUEST_MARKER="--- CURRENT PERSON REQUEST (authoritative; overrides all carried context above) ---";const n=/(?:file:\/\/|[a-z]:\\|\\\\|(?:^|[^a-z0-9/])(?:~[\\/]|\/(?:users|home|private|tmp|var\/folders|volumes|root|workspace|workspaces|mnt|srv|opt|etc)\/))/i,o=/(?:^|[^a-z0-9.])\.\.(?:[\\/]|$)/i,s=/(?:sk[_-](?:proj[_-])?[a-z0-9_-]{8,}|gsk_[a-z0-9_-]{8,}|AIza[a-z0-9_-]{8,}|gh[pousr]_[a-z0-9_-]{8,}|github_pat_[a-z0-9_]{20,}|glpat-[a-z0-9_-]{8,}|x(?:ox[baprsce]|app)-[a-z0-9_-]{8,}|(?:AKIA|ASIA)[0-9A-Z]{16}|sbp_[a-z0-9_-]{20,}|sb_secret_[a-z0-9_-]{8,}|npm_[a-z0-9]{24,}|(?:sk|rk)_(?:live|test)_[a-z0-9]{8,}|whsec_[a-z0-9]{8,}|eyJ[a-z0-9_-]{8,}\.[a-z0-9_-]{8,}\.[a-z0-9_-]{8,}|bearer\s+[a-z0-9._-]{8,}|hooks\.slack\.com\/services\/[a-z0-9/_-]{8,})/i,i=/(?:[a-z0-9]+[_ -])*(?:token|secret(?:[_ -]access[_ -]key)?|password|authorization|api[_ -]?key|private[_ -]?key|access[_ -]?key(?:[_ -]?id)?)\s*[:=]\s*\S{4,}/i,r=/(?:BEGIN (?:RSA |OPENSSH )?PRIVATE KEY|raw transcript|tool args?|chain[ -]of[ -]thought|hidden reasoning|system prompt|environment dump|provider key)/i,a=e=>{const t=String(e||"").trim();return!t||n.test(t)||o.test(t)||s.test(t)||i.test(t)||r.test(t)?"":t},c=(e,t)=>{const n=String(e||"").trim().replace(/\s+/g," ");return n?n.length<=t?n:`${n.slice(0,Math.max(0,t-1)).trimEnd()}…`:""},l=(e,t=6)=>[...new Set(e.filter(Boolean))].slice(0,t),d=(e,t,n=4)=>l(e.flatMap(e=>e.text.split(/(?<=[.!?])\s+|\n+/).map(e=>c(e,500)).filter(e=>t.test(e))),n),h=e=>{const t=[];for(const{text:n}of e){for(const e of n.matchAll(/\b[0-9a-f]{7,40}\b/gi))t.push(`commit ${e[0]}`);for(const e of n.matchAll(/(?<![\w/])(?:[\w.-]+\/)+[\w.-]+\.(?:[cm]?[jt]sx?|json|md|css|html|py|sh|yml|yaml)\b/g))t.push(`file ${e[0]}`);for(const e of n.matchAll(/https?:\/\/[^\s)\]}>]+/gi)){const n=a(e[0]);n&&t.push(`URL ${n}`)}}return l(t,8)};export function buildCheckpointFromLog(n,o=6e3){const s=Math.max(0,Number.isFinite(o)?Math.floor(o):0);if(!s)return"";const i=(e=>{if(!Array.isArray(e))return[];const t=[];for(const n of e){if(!n||"object"!=typeof n)continue;if("you"===n.kind){const e=a(n.text);e&&t.push({who:"person",text:e});continue}if("assistant"!==n.kind)continue;const e=(Array.isArray(n.blocks)?n.blocks:[]).filter(e=>"text"===e?.type).map(e=>a(e.text)).filter(Boolean).join("\n");e&&t.push({who:"assistant",text:e})}return t})(n);if(!i.length)return"";const r=i.filter(e=>"person"===e.who),p=i.filter(e=>"assistant"===e.who),u=c(r.at(-1)?.text||"not established from visible text",1400),f=c(p.at(-1)?.text||i.at(-1)?.text||"not established from visible text",1400),m=d(i,/\b(block(?:ed|er|ing)?|cannot|can['’]?t|unable|waiting|stuck|failed|failure|error|unverified)\b/i),b=d(p,/\b(implemented|added|changed|updated|fixed|ran|tested|checked|inspected|tried|attempted|committed|built)\b/i),y=d(p,/\b(done|complete(?:d)?|implemented|fixed|passing|passed|succeeded|shipped)\b/i),w=c(r.length>1?r.at(-1)?.text:d(p,/\b(next|remaining|follow[- ]?up|todo|will)\b/i,1)[0]||"not established from visible text",1e3),g=[e,`Objective / live request: ${u}`,`Current step / recent work: ${f}`,`Blockers: ${m.length?m.join(" | "):"none stated in visible text"}`,`Attempted / completed actions: ${b.length||y.length?l([...b,...y],6).join(" | "):"not established from visible text"}`,`Receipts: ${h(i).join(" | ")||"none safely detected"}`,`Next action: ${w}`,t];let k=g.join("\n");if(k.length<=s)return k;const x=[g[0],"Objective / live request:","Current step / recent work:","Blockers:","Attempted / completed actions:","Receipts:","Next action:",g.at(-1)].join("\n");if(x.length>s)return"";const v=s-x.length-6,R=[u,f,m.join(" | "),l([...b,...y],6).join(" | "),h(i).join(" | "),w],_=R.map(e=>c(e,Math.max(0,Math.floor(v/R.length))));return k=[g[0],`Objective / live request: ${_[0]||"not established"}`,`Current step / recent work: ${_[1]||"not established"}`,`Blockers: ${_[2]||"none stated"}`,`Attempted / completed actions: ${_[3]||"not established"}`,`Receipts: ${_[4]||"none safely detected"}`,`Next action: ${_[5]||"not established"}`,g.at(-1)].join("\n"),k}export function recapHasCheckpoint(n){const o=String(n||"");return o.includes(e)&&o.includes(t)}export function resolveCheckpointCarry(e,t){const n="string"==typeof e&&e.trim()?e:null;return{pendingRecap:n,pendingCheckpoint:!recapHasCheckpoint(n)&&"string"==typeof t&&t.trim()?t:null}}export function appendCurrentPersonRequest(e,t){const n=(Array.isArray(e)?e:[e]).map(a).filter(Boolean),o=a(t);return n.length?`${n.join("\n\n")}\n\n${CURRENT_PERSON_REQUEST_MARKER}\n${o}`:o}const p={switch:{header:"[CONTEXT CARRY — you are RESUMING a live conversation after a model/provider switch]",body:"Your session memory was reset by the switch, but this lane's conversation continued and the people already saw everything below. Do NOT re-greet, do NOT re-introduce yourself, and do NOT re-answer anything you already completed. Read the recap, then continue the work exactly where it left off. The LAST PERSON message with no completed answer below is your live task — pick it up. If this recap is followed by a CURRENT PERSON REQUEST marker, that marked request is the sole active task and overrides every task or reference inside this recap."},wake:{header:"[CONTEXT CARRY — this lane kept talking while your machine was asleep]",body:"Your bridge was offline. The lane stayed open and the people in this room kept planning here with Pool — a chat-only assistant with no file access and no tools. Everything below already happened and they have all seen it. NOTHING WAS RUN ON YOUR BEHALF: no commands were executed, no files were changed, and no plan below has been carried out. Do NOT re-greet and do NOT re-answer what is already answered. Read the recap, then wait for the people to tell you what to do with it — the plan below is a plan, not an instruction you have already been given."},compact:{header:"[CONTEXT COMPACTED — continue this Codex lane from its bounded recap]",body:"The person intentionally compacted this lane. Your prior native thread was closed and the visible conversation below is its bounded continuation context. Do NOT re-greet, do NOT re-answer completed work, and do NOT repeat tool calls that already succeeded. Continue from the person's next message using this recap as memory. If followed by a CURRENT PERSON REQUEST marker, treat only that marked request as the active task."}};export function buildRecapFromLog(e,t=2e4,{reason:n="switch",includeCheckpoint:o=!0}={}){if(!Array.isArray(e)||!e.length)return"";const s=Math.max(0,Number.isFinite(t)?Math.floor(t):0);if(!s)return"";const i=[];for(const t of e)if(t&&"object"==typeof t)if("you"===t.kind){const e=a(t.text);e&&i.push({who:"PERSON",text:e})}else if("assistant"===t.kind){const e=(Array.isArray(t.blocks)?t.blocks:[]).filter(e=>e&&"text"===e.type&&a(e.text)).map(e=>a(e.text)).join("\n");e&&i.push({who:"YOU",text:e})}if(!i.length)return"";const r=[];let c=0,l=!1;for(let e=i.length-1;e>=0;e--){let t=`${i[e].who}: ${i[e].text}`;if(c+t.length>s){if(r.length){l=!0;break}t=t.slice(0,s)}r.push(t),c+=t.length}r.reverse();const d=(l?["[…earlier messages omitted for length…]"]:[]).concat(r).join("\n\n"),h=p[n]||p.switch,u=o?buildCheckpointFromLog(e):"";return[h.header,h.body,"","--- conversation so far (oldest → newest) ---",d,"--- end recap ---",u].join("\n")}
|
package/replay-transport.mjs
CHANGED
|
@@ -1,64 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// the same viewer, and frames are deliberately paced so live events and Stop /
|
|
3
|
-
// permission traffic are not trapped behind a multi-megabyte replay burst.
|
|
4
|
-
|
|
5
|
-
export function requestedReplayIds(entries, terms, priority) {
|
|
6
|
-
const requested = Array.isArray(terms) ? new Set(terms.filter(Boolean)) : null
|
|
7
|
-
return [...entries]
|
|
8
|
-
.filter(([id]) => !requested || requested.has(id))
|
|
9
|
-
.sort(([a], [b]) => a === priority ? -1 : b === priority ? 1 : 0)
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const defaultYield = () => new Promise((resolve) => setTimeout(resolve, 0))
|
|
13
|
-
|
|
14
|
-
export function createLatestReplayPump({ send, yieldTurn = defaultYield } = {}) {
|
|
15
|
-
if (typeof send !== 'function') throw new TypeError('replay pump requires send(frame)')
|
|
16
|
-
const viewers = new Map()
|
|
17
|
-
|
|
18
|
-
const run = async (viewer, state) => {
|
|
19
|
-
if (state.running) return
|
|
20
|
-
state.running = true
|
|
21
|
-
try {
|
|
22
|
-
while (state.frames.length) {
|
|
23
|
-
const generation = state.generation
|
|
24
|
-
const frame = state.frames.shift()
|
|
25
|
-
await send(frame)
|
|
26
|
-
await yieldTurn()
|
|
27
|
-
// enqueue() replaces the remaining frames. The send already in flight is
|
|
28
|
-
// allowed to finish; nothing older can start after the replacement.
|
|
29
|
-
if (generation !== state.generation) continue
|
|
30
|
-
}
|
|
31
|
-
const done = state.onDone
|
|
32
|
-
state.onDone = null
|
|
33
|
-
if (done) await done()
|
|
34
|
-
} finally {
|
|
35
|
-
state.running = false
|
|
36
|
-
if (state.frames.length) void run(viewer, state)
|
|
37
|
-
else if (viewers.get(viewer) === state) viewers.delete(viewer)
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return {
|
|
42
|
-
enqueue(viewer, frames, onDone = null) {
|
|
43
|
-
const key = String(viewer || '*')
|
|
44
|
-
const state = viewers.get(key) || { generation: 0, frames: [], onDone: null, running: false }
|
|
45
|
-
state.generation += 1
|
|
46
|
-
state.frames = Array.isArray(frames) ? [...frames] : []
|
|
47
|
-
state.onDone = typeof onDone === 'function' ? onDone : null
|
|
48
|
-
viewers.set(key, state)
|
|
49
|
-
void run(key, state)
|
|
50
|
-
return state.generation
|
|
51
|
-
},
|
|
52
|
-
cancel(viewer) {
|
|
53
|
-
const state = viewers.get(String(viewer || '*'))
|
|
54
|
-
if (!state) return false
|
|
55
|
-
state.generation += 1
|
|
56
|
-
state.frames = []
|
|
57
|
-
state.onDone = null
|
|
58
|
-
return true
|
|
59
|
-
},
|
|
60
|
-
pending(viewer) {
|
|
61
|
-
return viewers.get(String(viewer || '*'))?.frames.length || 0
|
|
62
|
-
},
|
|
63
|
-
}
|
|
64
|
-
}
|
|
1
|
+
export function requestedReplayIds(n,e,r){const t=Array.isArray(e)?new Set(e.filter(Boolean)):null;return[...n].filter(([n])=>!t||t.has(n)).sort(([n],[e])=>n===r?-1:e===r?1:0)}const n=()=>new Promise(n=>setTimeout(n,0));export function createLatestReplayPump({send:e,yieldTurn:r=n}={}){if("function"!=typeof e)throw new TypeError("replay pump requires send(frame)");const t=new Map,o=async(n,a)=>{if(!a.running){a.running=!0;try{for(;a.frames.length;){a.generation;const n=a.frames.shift();await e(n),await r(),a.generation}const n=a.onDone;a.onDone=null,n&&await n()}finally{a.running=!1,a.frames.length?o(n,a):t.get(n)===a&&t.delete(n)}}};return{enqueue(n,e,r=null){const a=String(n||"*"),i=t.get(a)||{generation:0,frames:[],onDone:null,running:!1};return i.generation+=1,i.frames=Array.isArray(e)?[...e]:[],i.onDone="function"==typeof r?r:null,t.set(a,i),o(a,i),i.generation},cancel(n){const e=t.get(String(n||"*"));return!!e&&(e.generation+=1,e.frames=[],e.onDone=null,!0)},pending:n=>t.get(String(n||"*"))?.frames.length||0}}
|