@intentic/sandbox-contract 1.167.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 -2
- package/dist/agent-catalog.d.ts.map +1 -1
- package/dist/agent-catalog.js +80 -4
- 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 +37 -39
- package/dist/contracts/agents.contract.d.ts.map +1 -1
- package/dist/contracts/extensions.contract.d.ts +4 -0
- package/dist/contracts/extensions.contract.d.ts.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 +0 -2
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.d.ts +56 -0
- package/dist/contracts/system.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.js +7 -2
- package/dist/contracts/system.contract.js.map +1 -1
- package/dist/contracts/translator.contract.d.ts +36 -0
- package/dist/contracts/translator.contract.d.ts.map +1 -1
- package/dist/events.d.ts +88 -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 +185 -102
- 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 +168 -23
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +41 -17
- 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 +179 -15
- package/src/contracts/agent.contract.ts +1 -14
- package/src/contracts/system.contract.ts +15 -1
- package/src/events.test.ts +32 -0
- package/src/events.ts +105 -27
- package/src/index.ts +1 -0
- package/src/schemas.test.ts +2 -8
- package/src/schemas.ts +136 -57
- package/src/title.ts +6 -2
- package/src/workspace-state.test.ts +129 -0
- package/src/workspace-state.ts +160 -0
package/src/schemas.ts
CHANGED
|
@@ -262,6 +262,14 @@ export const AgentSummarySchema = z.object({
|
|
|
262
262
|
// Completed turns and lifetime tool calls — the card's msgs/tools counters.
|
|
263
263
|
turns: z.number().optional(),
|
|
264
264
|
toolUses: z.number().optional(),
|
|
265
|
+
/* The agents THIS agent started (SubagentSessionSchema), live and lifetime. Absent ⇒ it has never delegated,
|
|
266
|
+
* which is most agents — so the card's chip appears on content rather than reading "0" down the board.
|
|
267
|
+
*
|
|
268
|
+
* It earns a place on a card because a fleet card is the answer to "what is this agent up to", and an agent
|
|
269
|
+
* running five children looked exactly like an agent running none: the work was real, the spend was real, and
|
|
270
|
+
* the board said nothing. The tokens are NOT folded into the parent's cost — a child's spend is its own, and
|
|
271
|
+
* the Subagents area is where it is attributed. */
|
|
272
|
+
subagents: z.object({ running: z.number(), total: z.number() }).optional(),
|
|
265
273
|
// The agent's cumulative output (base → branch tip across every repo), refreshed on each land —
|
|
266
274
|
// the card's "12 files · +412 −96" readout. Independent of what has landed.
|
|
267
275
|
diff: z.object({ files: z.number(), insertions: z.number(), deletions: z.number() }).optional(),
|
|
@@ -383,9 +391,52 @@ export const AgentLandSchema = z.object({ id: z.string().min(1), mode: LandModeS
|
|
|
383
391
|
export const KeyedProviderSchema = z.enum(["codex", "grok", "kimi", "gemini"]);
|
|
384
392
|
export type KeyedProvider = z.infer<typeof KeyedProviderSchema>;
|
|
385
393
|
|
|
394
|
+
// ---- plan-limit usage ----
|
|
395
|
+
// Declared ABOVE both account shapes because both carry it: headroom is one idea in this product, not a Claude
|
|
396
|
+
// idea that other providers imitate. A native account (OauthAccount) and a routed subscription
|
|
397
|
+
// (TranslatorAccount) differ in who holds the credential and how the reading is taken — never in what a
|
|
398
|
+
// reading IS — so every surface that draws a percentage reads this one type and no other.
|
|
399
|
+
|
|
400
|
+
// One plan-limit pool. `kind` is the provider's own key ('five_hour' | 'seven_day' | 'seven_day_opus' |
|
|
401
|
+
// 'seven_day_sonnet' | 'model:Fable' | …) rather than an enum we'd have to keep in step with the provider: an
|
|
402
|
+
// unrecognised pool is shown under its raw key, which is far better than being silently folded into a
|
|
403
|
+
// neighbour. `label` is the provider's OWN display name where it supplies one (the per-model buckets do) — it
|
|
404
|
+
// wins over anything we'd infer, because the model names in a plan's limits are the provider's to rename.
|
|
405
|
+
// `resetsAt` is epoch SECONDS (matching the SDK's frame).
|
|
406
|
+
export const UsageWindowSchema = z.object({
|
|
407
|
+
kind: z.string(),
|
|
408
|
+
label: z.string().optional(),
|
|
409
|
+
utilization: z.number(), // 0-100
|
|
410
|
+
resetsAt: z.number().optional(),
|
|
411
|
+
});
|
|
412
|
+
export type UsageWindow = z.infer<typeof UsageWindowSchema>;
|
|
413
|
+
|
|
414
|
+
// An account's headroom: EVERY window the provider reports, read together, plus when the reading was taken.
|
|
415
|
+
// All of them, not the binding one, because "which pool is binding" changes between turns and a reader
|
|
416
|
+
// comparing accounts needs the same pools on every row. How the reading is TAKEN is per provider and stops at
|
|
417
|
+
// the daemon's readers: Claude's rides the turn's own stream, ChatGPT's and Google's are pulled through
|
|
418
|
+
// CLIProxyAPI's credential-scoped management call. All of them are control requests, so none costs tokens.
|
|
419
|
+
//
|
|
420
|
+
// Within one window utilization only climbs, so an un-reset window stays a valid FLOOR however old it is; past
|
|
421
|
+
// its `resetsAt` it describes a pool that no longer exists and the store drops it. `measuredAt` is epoch MS
|
|
422
|
+
// (matching connectedAt) — deliberately a different unit from the windows' seconds.
|
|
423
|
+
export const AccountUsageSchema = z.object({
|
|
424
|
+
windows: z.array(UsageWindowSchema),
|
|
425
|
+
measuredAt: z.number(),
|
|
426
|
+
});
|
|
427
|
+
export type AccountUsage = z.infer<typeof AccountUsageSchema>;
|
|
428
|
+
|
|
386
429
|
// One connected subscription in the translator. `name` is CLIProxyAPI's auth-file name — the stable store key a
|
|
387
430
|
// disconnect addresses — and `label` the sign-in identity it reported (the account email, else the file name).
|
|
388
|
-
export const TranslatorAccountSchema = z.object({
|
|
431
|
+
export const TranslatorAccountSchema = z.object({
|
|
432
|
+
name: z.string(),
|
|
433
|
+
label: z.string(),
|
|
434
|
+
// The same headroom an OauthAccount carries, on the same field, for the same reason: the account rows are
|
|
435
|
+
// one list to the reader. Optional because a provider whose quota this sandbox cannot read (Grok, Kimi) —
|
|
436
|
+
// or one that did not answer — must still render as the connected account it is, with a dot instead of a
|
|
437
|
+
// ring.
|
|
438
|
+
usage: AccountUsageSchema.optional(),
|
|
439
|
+
});
|
|
389
440
|
export type TranslatorAccount = z.infer<typeof TranslatorAccountSchema>;
|
|
390
441
|
// Which routed-provider subscriptions are connected in the translator, per provider — a LIST per provider, not
|
|
391
442
|
// a flag: CLIProxyAPI holds any number of auth files per provider side by side and balances requests across
|
|
@@ -453,14 +504,8 @@ export const SteerSchema = z
|
|
|
453
504
|
// True cancel for the conversation's in-flight turn — aborts the agent daemon-side, unlike closing the
|
|
454
505
|
// /agent fetch (which sends no cancel frame).
|
|
455
506
|
export const StopTurnSchema = z.object({ conversationId: z.string().min(1) });
|
|
456
|
-
|
|
457
|
-
//
|
|
458
|
-
// action a spent allowance offers when the sandbox holds more than one; omitted, the turn re-runs on
|
|
459
|
-
// whatever served it (a plain "try again now"). NOT_FOUND when nothing is pending (the failure was already
|
|
460
|
-
// superseded by a fresh turn, or the daemon restarted).
|
|
461
|
-
export const ResumeLimitSchema = z.object({ conversationId: z.string().min(1), account: z.string().min(1).optional() });
|
|
462
|
-
|
|
463
|
-
// ---- claude subscription usage ----
|
|
507
|
+
|
|
508
|
+
// ---- claude rate-limit gate ----
|
|
464
509
|
// The GATE signal: whether the provider is letting turns through right now, and — when it is refusing — which
|
|
465
510
|
// window is binding and when it lifts. This is the SDK's rate_limit_event, mapped one-to-one, and it is only
|
|
466
511
|
// ever about the CURRENT moment. It is deliberately NOT the thing the headroom displays read: the event names a
|
|
@@ -474,34 +519,6 @@ export const RateLimitInfoSchema = z.object({
|
|
|
474
519
|
});
|
|
475
520
|
export type RateLimitInfo = z.infer<typeof RateLimitInfoSchema>;
|
|
476
521
|
|
|
477
|
-
// One plan-limit pool. `kind` is the provider's own key ('five_hour' | 'seven_day' | 'seven_day_opus' |
|
|
478
|
-
// 'seven_day_sonnet' | 'model:Fable' | …) rather than an enum we'd have to keep in step with the provider: an
|
|
479
|
-
// unrecognised pool is shown under its raw key, which is far better than being silently folded into a
|
|
480
|
-
// neighbour. `label` is the provider's OWN display name where it supplies one (the per-model buckets do) — it
|
|
481
|
-
// wins over anything we'd infer, because the model names in a plan's limits are the provider's to rename.
|
|
482
|
-
// `resetsAt` is epoch SECONDS (matching the SDK's frame).
|
|
483
|
-
export const UsageWindowSchema = z.object({
|
|
484
|
-
kind: z.string(),
|
|
485
|
-
label: z.string().optional(),
|
|
486
|
-
utilization: z.number(), // 0-100
|
|
487
|
-
resetsAt: z.number().optional(),
|
|
488
|
-
});
|
|
489
|
-
export type UsageWindow = z.infer<typeof UsageWindowSchema>;
|
|
490
|
-
|
|
491
|
-
// An account's headroom: EVERY window the provider reports, read together, plus when the reading was taken.
|
|
492
|
-
// All of them, not the binding one, because "which pool is binding" changes between turns and a reader
|
|
493
|
-
// comparing accounts needs the same pools on every row. Sourced from the CLI's own usage endpoint at turn end
|
|
494
|
-
// (see claudeUsageWindows) — a control request, so it costs no tokens.
|
|
495
|
-
//
|
|
496
|
-
// Within one window utilization only climbs, so an un-reset window stays a valid FLOOR however old it is; past
|
|
497
|
-
// its `resetsAt` it describes a pool that no longer exists and the store drops it. `measuredAt` is epoch MS
|
|
498
|
-
// (matching connectedAt) — deliberately a different unit from the windows' seconds.
|
|
499
|
-
export const AccountUsageSchema = z.object({
|
|
500
|
-
windows: z.array(UsageWindowSchema),
|
|
501
|
-
measuredAt: z.number(),
|
|
502
|
-
});
|
|
503
|
-
export type AccountUsage = z.infer<typeof AccountUsageSchema>;
|
|
504
|
-
|
|
505
522
|
// ---- provider oauth ----
|
|
506
523
|
// Claude uses the PKCE authorize-URL + paste-back handshake (start → exchange). Codex uses OpenAI's device-code
|
|
507
524
|
// flow (start → poll): the browser signs in at verificationUri and enters userCode; the daemon polls until done.
|
|
@@ -528,8 +545,9 @@ export const OauthAccountSchema = z.object({
|
|
|
528
545
|
needsReauth: z.boolean().optional(),
|
|
529
546
|
detail: z.string().optional(),
|
|
530
547
|
// The account's last known subscription-usage snapshot, so the picker can show what's left on each account
|
|
531
|
-
// before the user commits a turn to one.
|
|
532
|
-
//
|
|
548
|
+
// before the user commits a turn to one. Absent until a reading exists for it — an unmeasured account reads
|
|
549
|
+
// as unknown, never 0%. Claude is the provider that fills it here, because its stream reports the windows;
|
|
550
|
+
// the routed subscriptions carry the identical field on TranslatorAccount, filled by a pulled reading.
|
|
533
551
|
usage: AccountUsageSchema.optional(),
|
|
534
552
|
});
|
|
535
553
|
export type OauthAccount = z.infer<typeof OauthAccountSchema>;
|
|
@@ -669,11 +687,6 @@ export const BuiltinPromptSchema = z.object({ base: z.enum(["intentic", "claude"
|
|
|
669
687
|
// surface fail to parse the moment a toggle is added — which reaches the user as a page of switches that are
|
|
670
688
|
// silently dead, not as an error. It also means an older on-disk manifest keeps the owner's other picks rather
|
|
671
689
|
// than being discarded whole.
|
|
672
|
-
// Build-wide kill switch for usage-limit auto-resume. Keep the setting and implementation in place while the
|
|
673
|
-
// feature is disabled, but clamp every daemon response and settings write to OFF so a persisted `true` from an
|
|
674
|
-
// older build cannot keep spending a newly reset allowance. The web also reads this constant to render the
|
|
675
|
-
// control unavailable and to distrust a stale daemon that still reports a scheduled resume.
|
|
676
|
-
export const USAGE_LIMIT_AUTO_RESUME_ENABLED: boolean = false;
|
|
677
690
|
|
|
678
691
|
export const SandboxSettingsSchema = z.object({
|
|
679
692
|
stableSystemPrompt: z.boolean().default(false),
|
|
@@ -746,18 +759,12 @@ export const SandboxSettingsSchema = z.object({
|
|
|
746
759
|
* automation-opened agents (Discord, webhooks, email) finish turns with no browser in the room — a
|
|
747
760
|
* browser-held preference could not govern them. Per-agent override: AgentSummarySchema.autoLand. */
|
|
748
761
|
autoLand: z.boolean().default(true),
|
|
749
|
-
// Latent opt-in for re-running a turn after the Claude subscription's usage window resets. It defaults off
|
|
750
|
-
// because an unattended retry spends the fresh allowance without the user in the room, and the build-wide
|
|
751
|
-
// gate above currently clamps even an older saved opt-in off while the feature is unavailable.
|
|
752
|
-
autoResumeOnLimit: z
|
|
753
|
-
.boolean()
|
|
754
|
-
.default(false)
|
|
755
|
-
.overwrite((enabled) => USAGE_LIMIT_AUTO_RESUME_ENABLED && enabled),
|
|
756
762
|
/* When a turn dies because the MODEL PROVIDER was failing (500/502/503, a 529 at capacity, a dropped
|
|
757
763
|
* socket), re-run it on an escalating backoff until it goes through or the attempts are spent.
|
|
758
764
|
*
|
|
759
|
-
* Defaults ON,
|
|
760
|
-
*
|
|
765
|
+
* Defaults ON, and a spent Claude allowance is the counter-example that explains why: that one is the
|
|
766
|
+
* user's own budget, and resuming into a freshly reset window spends something they may have been saving —
|
|
767
|
+
* so a usage limit stops the turn and says when it resets, and nothing re-runs it. An
|
|
761
768
|
* outage resume spends nothing the dead turn had not already committed, resolves in minutes rather than
|
|
762
769
|
* hours, and — the deciding argument — the turns hurt worst by it are the ones with nobody in the room
|
|
763
770
|
* (automation wakes, Discord, webhooks), which no browser-held preference could ever rescue. It is the same
|
|
@@ -765,8 +772,8 @@ export const SandboxSettingsSchema = z.object({
|
|
|
765
772
|
resumeAfterOutage: z.boolean().default(true),
|
|
766
773
|
/* When the daemon dies under a running turn, re-run that turn once it is back (agent/turn-journal.ts records
|
|
767
774
|
* every in-flight turn; the boot pass in agent/turn-resume.ts re-runs what survived). ON by default, where
|
|
768
|
-
*
|
|
769
|
-
* budget, while a restart is usually intentic's OWN doing — the container is recreated on every update,
|
|
775
|
+
* a spent usage limit re-runs nothing, and the difference is who broke the turn: a spent allowance is the
|
|
776
|
+
* user's own budget, while a restart is usually intentic's OWN doing — the container is recreated on every update,
|
|
770
777
|
* every environment approval and every dev-sandbox.sh swap. Approving the Dockerfile change an agent asked
|
|
771
778
|
* for must not cost the run that asked for it, and a user who just clicked Approve is in the room expecting
|
|
772
779
|
* the work to continue, not a second button.
|
|
@@ -797,7 +804,7 @@ export const SandboxSettingsSchema = z.object({
|
|
|
797
804
|
// gate exists to prevent is a green light nobody earned.
|
|
798
805
|
gateTimeoutMs: z.number().min(60_000).max(3_600_000).default(900_000),
|
|
799
806
|
/* Wake a fixer automatically when the gate goes red, instead of only lighting the badge. ON with a
|
|
800
|
-
* configured command,
|
|
807
|
+
* configured command, and the difference from the unattended spend a usage limit refuses is
|
|
801
808
|
* that the spend here is the POINT: a red gate whose fix waits for the user to notice has moved the CI
|
|
802
809
|
* round-trip into the workspace without removing it from the user's day. One attempt per verdict, so a
|
|
803
810
|
* command that fails for a reason no agent can fix costs one turn, not a loop (gate/gate.ts). */
|
|
@@ -2507,7 +2514,7 @@ export const BrowserPageSchema = z.object({
|
|
|
2507
2514
|
// The page's own title. Absent mid-navigation, which is exactly when a tab still needs to render.
|
|
2508
2515
|
title: z.string().optional(),
|
|
2509
2516
|
url: z.string(),
|
|
2510
|
-
// The page the agent last drove. Exactly one page
|
|
2517
|
+
// The page the agent last drove — on a finished session, the one it ended on. Exactly one page has it.
|
|
2511
2518
|
active: z.boolean(),
|
|
2512
2519
|
});
|
|
2513
2520
|
export const BrowserSessionSchema = z.object({
|
|
@@ -2521,6 +2528,9 @@ export const BrowserSessionSchema = z.object({
|
|
|
2521
2528
|
// still lists for a while, with the pages it had — the record of where the agent went.
|
|
2522
2529
|
running: z.boolean(),
|
|
2523
2530
|
activityAt: z.number(),
|
|
2531
|
+
// When that Chromium went away, for the "closed 20m ago" line a finished session leads with. Absent while
|
|
2532
|
+
// running, which is the same fact as `running` — but the view needs the timestamp, not just the flag.
|
|
2533
|
+
finishedAt: z.number().optional(),
|
|
2524
2534
|
pages: z.array(BrowserPageSchema),
|
|
2525
2535
|
});
|
|
2526
2536
|
export type BrowserPage = z.infer<typeof BrowserPageSchema>;
|
|
@@ -2529,6 +2539,75 @@ export const BrowsersListSchema = z.object({ sessions: z.array(BrowserSessionSch
|
|
|
2529
2539
|
export type BrowsersList = z.infer<typeof BrowsersListSchema>;
|
|
2530
2540
|
export const BrowserNameParamSchema = z.object({ name: z.string() });
|
|
2531
2541
|
|
|
2542
|
+
/* ---- subagents: the agents an agent starts ----
|
|
2543
|
+
*
|
|
2544
|
+
* The third thing a turn spawns that the operator can be shown, after its shell and its browser — and the only
|
|
2545
|
+
* one that is itself an agent. Two kinds land in this one list, because from outside they are the same fact
|
|
2546
|
+
* (another agent, working, that you did not start):
|
|
2547
|
+
* • `subagent` — the SDK's Agent/Task tool. The daemon learns of it from the SubagentStart/SubagentStop hooks
|
|
2548
|
+
* and the task_* stream messages, joined on `toolUseId`.
|
|
2549
|
+
* • `codex` / `grok` — a CLI the agent drove from its own Bash (agent/delegation.ts). Detected in the Bash
|
|
2550
|
+
* PreToolUse hook, bound to its thread/session id from the command's output.
|
|
2551
|
+
*
|
|
2552
|
+
* `id` IS THE SPAWNING TOOL CALL'S id — the Agent card's, or the Bash card's for a delegation. It is the one key
|
|
2553
|
+
* every source already carries (the SDK's subagent meta, its task_* messages, and the `parentToolUseId` the
|
|
2554
|
+
* client nests inner frames under), so nothing has to be correlated: a card links to its subagent with the id it
|
|
2555
|
+
* already has, and the subagent points back at the card the same way. The ids the transcripts are actually READ
|
|
2556
|
+
* with — the SDK's agent id, a Codex thread, an OpenCode session — stay daemon-side, because no surface asks a
|
|
2557
|
+
* question they answer.
|
|
2558
|
+
*
|
|
2559
|
+
* WHAT A KIND CHANGES, and it is only ever the live view: a subagent has no process of its own to look at, so
|
|
2560
|
+
* watching it means reading its transcript. A delegation runs in a tmux window, so it has both — `terminal`
|
|
2561
|
+
* names it, and the card keeps its existing "Watch in terminal" beside the transcript door. */
|
|
2562
|
+
export const SubagentKindSchema = z.enum(["subagent", "codex", "grok"]);
|
|
2563
|
+
export type SubagentKind = z.infer<typeof SubagentKindSchema>;
|
|
2564
|
+
|
|
2565
|
+
// running/pending are live; the rest are terminal. Deliberately the SDK's own task vocabulary
|
|
2566
|
+
// (SDKTaskUpdatedMessage.patch.status) rather than AgentStatus: this is not a fleet card's lifecycle (no
|
|
2567
|
+
// draft/landed/conflict), and mapping the two would invent states neither side reports.
|
|
2568
|
+
export const SubagentStatusSchema = z.enum(["pending", "running", "completed", "failed", "killed", "paused"]);
|
|
2569
|
+
export type SubagentStatus = z.infer<typeof SubagentStatusSchema>;
|
|
2570
|
+
|
|
2571
|
+
export const SubagentSessionSchema = z.object({
|
|
2572
|
+
id: z.string(),
|
|
2573
|
+
kind: SubagentKindSchema,
|
|
2574
|
+
// The conversation whose turn spawned this — what the area groups its rows by, and the way back to the chat
|
|
2575
|
+
// the card lives in.
|
|
2576
|
+
conversationId: z.string(),
|
|
2577
|
+
// What it is and what it was asked to do: the subagent type (`Explore`, `general-purpose`) or the delegated
|
|
2578
|
+
// provider's model, and the caller's one-line description. The area's row and the card's title read as
|
|
2579
|
+
// `Explore · Locate claimIndexer definition`.
|
|
2580
|
+
agentType: z.string().optional(),
|
|
2581
|
+
description: z.string().optional(),
|
|
2582
|
+
model: z.string().optional(),
|
|
2583
|
+
// How deep in the spawn tree (1 = spawned by the turn itself). From the SDK's meta.json; a subagent may
|
|
2584
|
+
// itself delegate, and a flat list that cannot say so reads as though the turn started all of them.
|
|
2585
|
+
spawnDepth: z.number().optional(),
|
|
2586
|
+
// Backgrounded: the parent went on working instead of waiting for it. This is the whole reason the list
|
|
2587
|
+
// exists — a backgrounded child used to be invisible until its result landed, sometimes minutes later.
|
|
2588
|
+
background: z.boolean().optional(),
|
|
2589
|
+
status: SubagentStatusSchema,
|
|
2590
|
+
startedAt: z.number(),
|
|
2591
|
+
endedAt: z.number().optional(),
|
|
2592
|
+
activityAt: z.number(),
|
|
2593
|
+
// What it has spent and done so far (task_progress). Tokens are the child's own, so a parent's cost line and
|
|
2594
|
+
// the sum of its children's are two different true numbers.
|
|
2595
|
+
tokens: z.number().optional(),
|
|
2596
|
+
toolUses: z.number().optional(),
|
|
2597
|
+
lastTool: z.string().optional(),
|
|
2598
|
+
// Its report — the last assistant message (SubagentStop) or the task summary. The answer to "what did it
|
|
2599
|
+
// conclude?" without opening the transcript, which is the question a finished child is read for.
|
|
2600
|
+
summary: z.string().optional(),
|
|
2601
|
+
error: z.string().optional(),
|
|
2602
|
+
// A delegation's live view: the tmux session its command runs in. Absent for an SDK subagent, which has no
|
|
2603
|
+
// process of its own to attach to.
|
|
2604
|
+
terminal: z.string().optional(),
|
|
2605
|
+
});
|
|
2606
|
+
export type SubagentSession = z.infer<typeof SubagentSessionSchema>;
|
|
2607
|
+
export const SubagentsListSchema = z.object({ sessions: z.array(SubagentSessionSchema) });
|
|
2608
|
+
export type SubagentsList = z.infer<typeof SubagentsListSchema>;
|
|
2609
|
+
export const SubagentIdParamSchema = z.object({ id: z.string() });
|
|
2610
|
+
|
|
2532
2611
|
// ---- environment: the overlay Dockerfile extending the sandbox image ----
|
|
2533
2612
|
// The approved file is DAEMON-COMPOSED: pinned FROM + capability fragments + the owner-approved custom section.
|
|
2534
2613
|
// The agent writes the proposal file (.intentic/environment.Dockerfile — custom-section content only, no FROM)
|
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
|
+
];
|