@intentic/sandbox-contract 1.166.0 → 1.168.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/dist/agent-catalog.d.ts +20 -3
- package/dist/agent-catalog.d.ts.map +1 -1
- package/dist/agent-catalog.js +81 -6
- package/dist/agent-catalog.js.map +1 -1
- package/dist/contracts/agent.contract.d.ts +36 -8
- package/dist/contracts/agent.contract.d.ts.map +1 -1
- package/dist/contracts/agent.contract.js +1 -2
- package/dist/contracts/agent.contract.js.map +1 -1
- package/dist/contracts/agents.contract.d.ts +55 -39
- package/dist/contracts/agents.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.js.map +1 -1
- package/dist/contracts/automations.contract.d.ts +1 -1
- package/dist/contracts/extensions.contract.d.ts +4 -0
- package/dist/contracts/extensions.contract.d.ts.map +1 -1
- package/dist/contracts/gate.contract.d.ts +1 -1
- package/dist/contracts/kimi.contract.d.ts +0 -70
- package/dist/contracts/kimi.contract.d.ts.map +1 -1
- package/dist/contracts/kimi.contract.js +1 -5
- package/dist/contracts/kimi.contract.js.map +1 -1
- package/dist/contracts/sessions.contract.d.ts +1 -39
- package/dist/contracts/sessions.contract.d.ts.map +1 -1
- package/dist/contracts/settings.contract.d.ts +29 -5
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.d.ts +78 -4
- package/dist/contracts/system.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.js +9 -2
- package/dist/contracts/system.contract.js.map +1 -1
- package/dist/contracts/translator.contract.d.ts +47 -0
- package/dist/contracts/translator.contract.d.ts.map +1 -1
- package/dist/contracts/translator.contract.js.map +1 -1
- package/dist/events.d.ts +92 -159
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +37 -4
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +267 -181
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/schemas.d.ts +269 -44
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +92 -25
- package/dist/schemas.js.map +1 -1
- package/dist/title.d.ts.map +1 -1
- package/dist/title.js.map +1 -1
- package/dist/workspace-state.d.ts +9 -0
- package/dist/workspace-state.d.ts.map +1 -0
- package/dist/workspace-state.js +71 -0
- package/dist/workspace-state.js.map +1 -0
- package/package.json +2 -2
- package/src/agent-catalog.test.ts +118 -0
- package/src/agent-catalog.ts +183 -24
- package/src/contracts/agent.contract.ts +1 -14
- package/src/contracts/agents.contract.ts +8 -6
- package/src/contracts/gate.contract.ts +2 -2
- package/src/contracts/kimi.contract.ts +4 -21
- package/src/contracts/system.contract.ts +24 -1
- package/src/contracts/translator.contract.ts +4 -6
- package/src/events.test.ts +32 -0
- package/src/events.ts +105 -26
- package/src/index.ts +1 -0
- package/src/model-order.ts +1 -1
- package/src/quick-model.test.ts +1 -1
- package/src/schemas.test.ts +8 -4
- package/src/schemas.ts +296 -111
- package/src/title.ts +6 -2
- package/src/workspace-state.test.ts +129 -0
- package/src/workspace-state.ts +160 -0
package/src/title.ts
CHANGED
|
@@ -15,8 +15,12 @@
|
|
|
15
15
|
* names a turn that arrived without one — an automation, a Discord message, a webchat visitor (agents-
|
|
16
16
|
* registry.ts). One rule, because two would let the same prompt open under two different names depending on
|
|
17
17
|
* where it entered. Nothing here calls a model: the title has to exist before the first frame comes back.
|
|
18
|
-
*
|
|
19
|
-
*
|
|
18
|
+
*
|
|
19
|
+
* Which is exactly the ceiling on it. Cutting is not naming — what comes out is the user's own sentence,
|
|
20
|
+
* shortened, and a column of those is scannable only where the users' sentences happened to differ early. The
|
|
21
|
+
* name a conversation ends up WEARING is written a second or two later by a model that reads the same prompt
|
|
22
|
+
* and answers in the fleet board's own shape (the daemon's title-namer.ts). This is the title that holds the
|
|
23
|
+
* tab until that arrives, and the one it keeps if nothing is connected to write a better one. */
|
|
20
24
|
|
|
21
25
|
// The registry's title budget (agents-registry MAX_TITLE_LENGTH, the rename input's maxlength) — the widest
|
|
22
26
|
// any surface stores. Every surface truncates in CSS to its own width, so the clamp here is a storage cap,
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { FileContribution } from "@intentic/extension-api";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { staleQueryKeys, WORKSPACE_STATE_FILES } from "./workspace-state.js";
|
|
4
|
+
|
|
5
|
+
// What the automations extension declares in its manifest, and what the memory extension WOULD declare if the
|
|
6
|
+
// watcher reported its files. Literals rather than the real manifests: an extension package importing this one is
|
|
7
|
+
// the dependency direction, so reaching back for them here would invert it. The real manifests are checked
|
|
8
|
+
// against this rule where they are loaded — web's fileBindings.test.ts and the daemon's file-bindings.test.ts.
|
|
9
|
+
const AUTOMATIONS: readonly FileContribution[] = [
|
|
10
|
+
{ path: `.intentic/automations.json`, invalidates: [`automations`] },
|
|
11
|
+
{ path: `.intentic/approvals/`, invalidates: [`automation-approvals`] },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
describe(`staleQueryKeys`, () => {
|
|
15
|
+
it(`maps a manifest write to the queries it makes stale`, () => {
|
|
16
|
+
expect(staleQueryKeys([`.intentic/capabilities.json`], [])).toEqual([`capabilities`, `environment`, `panels`]);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it(`matches a name family and a one-file-per-entry directory through one prefix each`, () => {
|
|
20
|
+
// environment.Dockerfile, environment.custom.Dockerfile, environment.approved.Dockerfile — one entry.
|
|
21
|
+
expect(staleQueryKeys([`.intentic/environment.custom.Dockerfile`], [])).toEqual([`environment`]);
|
|
22
|
+
expect(staleQueryKeys([`.intentic/drafts/post-1.json`], [])).toEqual([`drafts`]);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it(`refreshes the Drafts view when the AGENT writes a draft`, () => {
|
|
26
|
+
// The regression this table was reorganized around: the drafts skill writes these files directly, so
|
|
27
|
+
// there is no browser mutation to hang an invalidate on — the watcher push is the only signal, and it
|
|
28
|
+
// used to be dropped on the floor.
|
|
29
|
+
expect(staleQueryKeys([`.intentic/drafts/post-1.json`], [])).toEqual([`drafts`]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it(`ignores unrelated churn under .intentic/`, () => {
|
|
33
|
+
// The amplification that turned an iq index rebuild into an endless request storm: a prefix test on
|
|
34
|
+
// `.intentic/` alone would invalidate every one of these queries for each index write.
|
|
35
|
+
expect(staleQueryKeys([`.intentic/iq/index.db`, `.intentic/claude/projects/p/session.jsonl`], [])).toEqual([]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it(`ignores a store's own temp file while it is mid-swap`, () => {
|
|
39
|
+
// jsonFile writes `.<name>.<pid>.tmp` beside the target precisely so the atomic rename can't be read as
|
|
40
|
+
// a write to the target itself. A trailing-tag temp would prefix-match and bill an extra refetch.
|
|
41
|
+
expect(staleQueryKeys([`.intentic/.settings.json.42.tmp`], [])).toEqual([]);
|
|
42
|
+
expect(staleQueryKeys([`.intentic/settings.json`], [])).toEqual([`settings`]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it(`ignores ordinary workspace edits`, () => {
|
|
46
|
+
expect(staleQueryKeys([`src/main.ts`, `README.md`], [])).toEqual([]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it(`dedupes keys across a batch that touches several manifests`, () => {
|
|
50
|
+
// A capability add recomposes the overlay, so both entries claim `environment` — one refetch, not two.
|
|
51
|
+
expect(staleQueryKeys([`.intentic/capabilities.json`, `.intentic/environment.Dockerfile`], [])).toEqual([
|
|
52
|
+
`capabilities`,
|
|
53
|
+
`environment`,
|
|
54
|
+
`panels`,
|
|
55
|
+
]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it(`invalidates an extension's queries from its own declaration`, () => {
|
|
59
|
+
expect(staleQueryKeys([`.intentic/automations.json`], AUTOMATIONS)).toEqual([`automations`]);
|
|
60
|
+
expect(staleQueryKeys([`.intentic/approvals/a1.json`], AUTOMATIONS)).toEqual([`automation-approvals`]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it(`makes nothing stale for an extension that is not running`, () => {
|
|
64
|
+
// The reason the live set is passed in rather than read off the installed list: `automations` is the
|
|
65
|
+
// extension's query key, so with the extension gone there is no cache entry for it to be about. The core
|
|
66
|
+
// table used to carry these two keys itself, and would have kept invalidating them either way.
|
|
67
|
+
expect(staleQueryKeys([`.intentic/automations.json`, `.intentic/approvals/a1.json`], [])).toEqual([]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it(`lets an extension claim a path the core table deliberately ignores`, () => {
|
|
71
|
+
// The two lists are unioned flat, not layered: a narrow extension entry under a broad core entry that
|
|
72
|
+
// invalidates nothing must still fire. Without this, every path beneath one of the daemon's
|
|
73
|
+
// machine-state prefixes would be unreachable to extensions.
|
|
74
|
+
const nested: readonly FileContribution[] = [{ path: `.intentic/claude/projects/p/memory/`, invalidates: [`memory`] }];
|
|
75
|
+
expect(staleQueryKeys([`.intentic/claude/projects/p/memory/note.md`], nested)).toEqual([`memory`]);
|
|
76
|
+
// …and a sibling under the same core prefix stays ignored.
|
|
77
|
+
expect(staleQueryKeys([`.intentic/claude/projects/p/session.jsonl`], nested)).toEqual([]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it(`dedupes a key two extensions both claim`, () => {
|
|
81
|
+
const twice: readonly FileContribution[] = [
|
|
82
|
+
{ path: `.intentic/automations.json`, invalidates: [`automations`] },
|
|
83
|
+
{ path: `.intentic/automations.json`, invalidates: [`automations`] },
|
|
84
|
+
];
|
|
85
|
+
expect(staleQueryKeys([`.intentic/automations.json`], twice)).toEqual([`automations`]);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe(`WORKSPACE_STATE_FILES`, () => {
|
|
90
|
+
it(`declares every entry under .intentic/, root-relative and forward-slash`, () => {
|
|
91
|
+
for (const file of WORKSPACE_STATE_FILES) {
|
|
92
|
+
expect(file.path.startsWith(`.intentic/`), file.path).toBe(true);
|
|
93
|
+
expect(file.path.includes(`\\`), file.path).toBe(false);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it(`states a reason for every entry that invalidates nothing`, () => {
|
|
98
|
+
// An empty `invalidates` is a real answer (daemon machine state, a deliberately-polled surface, a path
|
|
99
|
+
// whose query keys belong to an extension), but a SILENT one is indistinguishable from the omission this
|
|
100
|
+
// table exists to prevent — which is exactly how drafts went missing. Requiring the reason is what makes
|
|
101
|
+
// the difference visible at review time.
|
|
102
|
+
for (const file of WORKSPACE_STATE_FILES) {
|
|
103
|
+
if (file.invalidates.length === 0) {
|
|
104
|
+
expect(file.why, `${file.path} invalidates nothing and must say why`).toBeTruthy();
|
|
105
|
+
} else {
|
|
106
|
+
expect(file.why, `${file.path} invalidates queries, so \`why\` is dead weight`).toBeUndefined();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it(`keeps directory entries slash-terminated so they cannot swallow a sibling`, () => {
|
|
112
|
+
// `.intentic/drafts` without the slash would also prefix-match a future `.intentic/drafts-archive.json`.
|
|
113
|
+
for (const file of WORKSPACE_STATE_FILES.filter((entry) => entry.invalidates.length > 0)) {
|
|
114
|
+
const isFamilyPrefix = file.path.endsWith(`.`);
|
|
115
|
+
const isFile = file.path.endsWith(`.json`) || file.path.endsWith(`.Dockerfile`);
|
|
116
|
+
expect(isFile || isFamilyPrefix || file.path.endsWith(`/`), file.path).toBe(true);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it(`has no entry that prefix-matches another, so one write can't be billed twice`, () => {
|
|
121
|
+
for (const file of WORKSPACE_STATE_FILES) {
|
|
122
|
+
const overlapping = WORKSPACE_STATE_FILES.filter((other) => other !== file && other.path.startsWith(file.path));
|
|
123
|
+
expect(
|
|
124
|
+
overlapping.map((other) => other.path),
|
|
125
|
+
`${file.path} is a prefix of another entry`,
|
|
126
|
+
).toEqual([]);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import type { FileContribution } from "@intentic/extension-api";
|
|
2
|
+
|
|
3
|
+
/* WHICH WORKSPACE FILE BACKS WHICH CORE VIEW — one declaration, read by both sides of the wire.
|
|
4
|
+
*
|
|
5
|
+
* The daemon's own state lives under `<workspace>/.intentic/`, the agent edits it out-of-band with its file
|
|
6
|
+
* tools, and the file watcher pushes every change as a `workspaceChanged` batch. Turning those paths back into
|
|
7
|
+
* "and therefore this view is stale" used to be a hand-written table in the BROWSER (web's systemEventRouting),
|
|
8
|
+
* maintained separately from the paths the daemon actually writes (composition.ts) — two lists of the same
|
|
9
|
+
* fact, in two packages, with nothing tying them together.
|
|
10
|
+
*
|
|
11
|
+
* They drifted, exactly as that shape always does. `.intentic/drafts/` is written by the AGENT (the drafts
|
|
12
|
+
* skill puts a file there) and rendered by the Drafts view, but it was never added to the browser's table — so
|
|
13
|
+
* a draft appearing on disk while the owner watched the page changed nothing until they refocused the tab.
|
|
14
|
+
* Extension settings and the members list were missing for the same reason; writing them out is what showed
|
|
15
|
+
* that neither is a drafts-shaped hole — see their entries.
|
|
16
|
+
*
|
|
17
|
+
* So the binding is declared HERE, once, in the package both the daemon and the browser already import, and
|
|
18
|
+
* each side derives what it needs: the daemon builds its store paths from `path`, the browser builds its
|
|
19
|
+
* invalidation table from `invalidates`. Adding a manifest without saying what it makes stale is now a change
|
|
20
|
+
* to one visible list rather than an omission in a file nobody edits — and `workspace-state.test.ts` fails when
|
|
21
|
+
* a daemon store names a `.intentic` path this list doesn't carry.
|
|
22
|
+
*
|
|
23
|
+
* This mirrors what routes.ts does for the route surface ("nothing is generated and nothing is hand-maintained")
|
|
24
|
+
* one layer over: the same refusal to keep the same knowledge in two places.
|
|
25
|
+
*
|
|
26
|
+
* EXTENSIONS declare their own half in their manifest (`contributes.files`, @intentic/extension-api), in the same
|
|
27
|
+
* two fields, and the browser unions the two lists — see staleQueryKeys. That split is what this table is FOR:
|
|
28
|
+
* before it existed the core enumeration had to carry `automations` and `automation-approvals`, query keys owned
|
|
29
|
+
* by the automations extension, because the extension had no way to say so itself. A key belongs to whoever
|
|
30
|
+
* queries it. */
|
|
31
|
+
|
|
32
|
+
// A core entry is an extension's `contributes.files` entry plus the one thing only the core list needs: the
|
|
33
|
+
// right to declare NO invalidations, which for a daemon-owned file is the answer more often than not.
|
|
34
|
+
export interface WorkspaceStateFile {
|
|
35
|
+
/* Workspace-root-relative, forward-slash — the space `workspaceChanged` paths arrive in. Matching is by
|
|
36
|
+
* PREFIX, which lets one entry cover three shapes without a second matching rule:
|
|
37
|
+
* - an exact file `.intentic/settings.json`
|
|
38
|
+
* - a directory `.intentic/drafts/` (one file per draft)
|
|
39
|
+
* - a name family `.intentic/environment.` (…Dockerfile, .custom.Dockerfile, .approved.Dockerfile)
|
|
40
|
+
* A directory entry keeps its trailing slash so it can never prefix-match a sibling file. */
|
|
41
|
+
readonly path: string;
|
|
42
|
+
/* The browser query keys this file's contents feed. EMPTY is a real answer, not a gap — a file the browser
|
|
43
|
+
* renders nothing from, or one deliberately kept off the push path — and `why` says which. Never a prefix
|
|
44
|
+
* test over `.intentic/` as a whole: one stray write must not cost every view a refetch, which is the
|
|
45
|
+
* amplification that once turned an iq index rebuild into an endless request storm. */
|
|
46
|
+
readonly invalidates: readonly string[];
|
|
47
|
+
// Why this file has no invalidations, for the entries that declare none. Absent when it has some.
|
|
48
|
+
readonly why?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const WORKSPACE_STATE_FILES: readonly WorkspaceStateFile[] = [
|
|
52
|
+
// A capability add/remove recomposes the environment overlay and can add or drop a repo's panel.
|
|
53
|
+
{ path: ".intentic/capabilities.json", invalidates: ["capabilities", "environment", "panels"] },
|
|
54
|
+
{ path: ".intentic/environment.", invalidates: ["environment"] },
|
|
55
|
+
{ path: ".intentic/settings.json", invalidates: ["settings"] },
|
|
56
|
+
// Written by the AGENT's file tools (the drafts skill), read by the owner's approval inbox — the one entry
|
|
57
|
+
// here whose whole point is that a change arrives from outside the browser that renders it.
|
|
58
|
+
{ path: ".intentic/drafts/", invalidates: ["drafts"] },
|
|
59
|
+
// ---- declared by the extension that renders them (contributes.files), not here ----
|
|
60
|
+
// The path is the DAEMON's (automations-store writes both), the query keys are the intentic.automations
|
|
61
|
+
// extension's. It declares them in its own manifest and the browser unions the two lists, so uninstalling
|
|
62
|
+
// the extension takes its invalidations with it instead of leaving a rule for a view that no longer exists.
|
|
63
|
+
{
|
|
64
|
+
path: ".intentic/automations.json",
|
|
65
|
+
invalidates: [],
|
|
66
|
+
why: "Declared by the intentic.automations extension's contributes.files — `automations` is its query key, not core's.",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
path: ".intentic/approvals/",
|
|
70
|
+
invalidates: [],
|
|
71
|
+
why: "Declared by the intentic.automations extension's contributes.files — `automation-approvals` is its query key, not core's.",
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
/* ---- reached by no query, for reasons that are not oversights ----
|
|
75
|
+
*
|
|
76
|
+
* This channel's currency is a QUERY KEY, and invalidation only reaches a query something is observing.
|
|
77
|
+
* Both entries below are outside that by design, so an empty set is the honest record — naming a key no
|
|
78
|
+
* query uses would put the drift this table exists to remove straight back into it. Each says which
|
|
79
|
+
* constraint would have to move first, so the next reader doesn't re-derive it. */
|
|
80
|
+
{
|
|
81
|
+
path: ".intentic/extension-settings.json",
|
|
82
|
+
invalidates: [],
|
|
83
|
+
why: "Held in a module-level shallowRef store per extension (web's extensionSettingsStore) with no query observer, and deliberately so: api.settings.get must answer SYNCHRONOUSLY from an extension's first activate() line, and the store outlives every component scope. A module-level QueryObserver is the one shape that would make invalidation refetch, and this app already ruled it out — it detaches on the queryClient.clear() at logout (see useSandbox's sandbox-list mirror). So a remote member's setting edit reaches this browser on its next load, not live.",
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
path: ".intentic/members.json",
|
|
87
|
+
invalidates: [],
|
|
88
|
+
why: "Not this view's source at all: SandboxAccess renders the PLATFORM's invite records (apiClient.invite.list), and this file is the daemon's ENFORCED copy — written first so a grant the enforcer never got is never recorded, then never read back. A change here means the two disagreed, which the write order makes fail-closed rather than stale.",
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
// ---- daemon-owned, nothing derives from watching them ----
|
|
92
|
+
{
|
|
93
|
+
path: ".intentic/gate.json",
|
|
94
|
+
invalidates: [],
|
|
95
|
+
why: "The landing gate's verdict is POLLED on purpose (web's useGate). Its fingerprint pass rewrites this file every couple of seconds while a check runs, and pushing that back would refetch the review set — the daemon's most expensive read — on every poll.",
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
path: ".intentic/gate-index/",
|
|
99
|
+
invalidates: [],
|
|
100
|
+
why: "The gate's per-repo git index. Machine state, rewritten continuously by the fingerprint pass.",
|
|
101
|
+
},
|
|
102
|
+
/* Agent session transcripts, rewritten on every streamed token.
|
|
103
|
+
*
|
|
104
|
+
* The memory notes under it (`projects/<slug>/memory/**`) ARE user-facing and the /memory view polls them
|
|
105
|
+
* every 30s, which is the one place in this table where a poll survives a real change feed being available.
|
|
106
|
+
* It stays a poll deliberately: the watcher's exclusion is a DESCENT filter, so reaching those notes means
|
|
107
|
+
* letting it walk `.intentic/claude` → `projects` → every project slug. Measured on the live workspace that
|
|
108
|
+
* is +119 watched directories against ~593 today (a fifth more), with 314 continuously-rewritten transcripts
|
|
109
|
+
* inside the newly-watched set, to make ONE memory directory live. Notes change at agent-turn cadence, so the
|
|
110
|
+
* poll costs a request a minute and the alternative costs a permanent 20% on the watcher. */
|
|
111
|
+
{
|
|
112
|
+
path: ".intentic/claude/",
|
|
113
|
+
invalidates: [],
|
|
114
|
+
why: "Agent session transcripts — see the note above on why the memory notes under it stay polled.",
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
path: ".intentic/ci.json",
|
|
118
|
+
invalidates: [],
|
|
119
|
+
why: "Webhook secret + conclusion memory; the Pipelines view reads it through /ci/runs, not off disk.",
|
|
120
|
+
},
|
|
121
|
+
{ path: ".intentic/bridge-tokens.json", invalidates: [], why: "Hashed ACP bridge tokens, listed on demand by the owner." },
|
|
122
|
+
{
|
|
123
|
+
path: ".intentic/owner.json",
|
|
124
|
+
invalidates: [],
|
|
125
|
+
why: "Bound once on first use; a change here means the sandbox was re-owned, which re-authenticates anyway.",
|
|
126
|
+
},
|
|
127
|
+
{ path: ".intentic/workspace.json", invalidates: [], why: "The workspace identity, read from the /events hello frame rather than as a file." },
|
|
128
|
+
{ path: ".intentic/templates.json", invalidates: [], why: "Scaffold templates, read when the scaffold dialog opens." },
|
|
129
|
+
{
|
|
130
|
+
path: ".intentic/browser/",
|
|
131
|
+
invalidates: [],
|
|
132
|
+
why: "Browser-login profiles: Chromium rewrites these constantly. Descent-ignored by the watcher outright.",
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
path: ".intentic/extensions/",
|
|
136
|
+
invalidates: [],
|
|
137
|
+
why: "Extension checkouts — whole git clones. The `extensions` query is driven by the capability manifest above, not by their contents.",
|
|
138
|
+
},
|
|
139
|
+
{ path: ".intentic/plugins/", invalidates: [], why: "Agent plugin dirs, read by the SDK's loader each turn." },
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
/* The query keys a batch of changed paths makes stale, deduped and stable. The browser's `/events` handler calls
|
|
143
|
+
* this; keeping it here rather than in the web means the rule is unit-testable without a query client, and the
|
|
144
|
+
* daemon can assert against the same table.
|
|
145
|
+
*
|
|
146
|
+
* `contributed` is what the ACTIVATED extensions declared in `contributes.files` — passed in rather than
|
|
147
|
+
* imported, because which extensions are live is a browser fact this package has no way to know. It is a
|
|
148
|
+
* required argument for the same reason: an added second source that callers may forget is a source that
|
|
149
|
+
* silently does nothing, which is the failure this whole file exists to remove. Extension entries are unioned
|
|
150
|
+
* flat with the core ones, not layered over them: both lists describe the same fact about the same file, and a
|
|
151
|
+
* path can legitimately match one entry in each — a core prefix that invalidates nothing must not veto a
|
|
152
|
+
* narrower extension entry beneath it, or everything under one of the daemon's machine-state prefixes would be
|
|
153
|
+
* unreachable to extensions by construction. */
|
|
154
|
+
export const staleQueryKeys = (paths: readonly string[], contributed: readonly FileContribution[]): readonly string[] => [
|
|
155
|
+
...new Set(
|
|
156
|
+
[...WORKSPACE_STATE_FILES, ...contributed]
|
|
157
|
+
.filter((file) => file.invalidates.length > 0 && paths.some((path) => path.startsWith(file.path)))
|
|
158
|
+
.flatMap((file) => file.invalidates),
|
|
159
|
+
),
|
|
160
|
+
];
|