@agimon-ai/po-core 0.0.0-alpha.115 → 0.0.0-alpha.116

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.
@@ -37,6 +37,23 @@ const SOURCE_ORDER = {
37
37
  plugin: 2,
38
38
  core: 3
39
39
  };
40
+ /** Reads only replayable selection fields, accepting legacy projections without a request marker. */
41
+ function readProjectedSelection(data) {
42
+ if (typeof data !== "object" || data === null || Array.isArray(data) || !("selection" in data)) return void 0;
43
+ const selection = data.selection;
44
+ if (typeof selection !== "object" || selection === null || Array.isArray(selection)) return void 0;
45
+ if (!("majorMode" in selection) || typeof selection.majorMode !== "string") return void 0;
46
+ if (!("domains" in selection) || !Array.isArray(selection.domains) || !selection.domains.every((domain) => typeof domain === "string")) return void 0;
47
+ const profile = "profile" in selection ? selection.profile : void 0;
48
+ if (profile !== void 0 && typeof profile !== "string") return void 0;
49
+ const requestedProfile = "requestedProfile" in selection ? selection.requestedProfile : void 0;
50
+ return {
51
+ majorMode: selection.majorMode,
52
+ domains: [...selection.domains],
53
+ profile,
54
+ ...typeof requestedProfile === "string" || requestedProfile === null ? { requestedProfile } : {}
55
+ };
56
+ }
40
57
  function skillSource(group) {
41
58
  if (group === "plugins") return "plugin";
42
59
  if (group === "extensions") return "extension";
@@ -107,7 +124,8 @@ function projectContext(input) {
107
124
  selection: {
108
125
  majorMode: input.majorMode,
109
126
  domains: [...input.domains],
110
- ...input.profile === void 0 ? {} : { profile: input.profile }
127
+ ...input.profile === void 0 ? {} : { profile: input.profile },
128
+ ...input.requestedProfile === void 0 ? {} : { requestedProfile: input.requestedProfile }
111
129
  },
112
130
  groups,
113
131
  totalTokens: groups.reduce((total, group) => total + group.tokens, 0),
@@ -123,5 +141,6 @@ exports.PO_CONFIG_ENTRY_TYPE = PO_CONFIG_ENTRY_TYPE;
123
141
  exports.PO_CONFIG_TRANSITION_ENTRY_TYPE = PO_CONFIG_TRANSITION_ENTRY_TYPE;
124
142
  exports.PO_CONTEXT_ENTRY_TYPE = PO_CONTEXT_ENTRY_TYPE;
125
143
  exports.projectContext = projectContext;
144
+ exports.readProjectedSelection = readProjectedSelection;
126
145
 
127
146
  //# sourceMappingURL=contextProjection.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"contextProjection.cjs","names":["tokensForTool"],"sources":["../src/services/contextProjection/index.ts"],"sourcesContent":["/** What admitted a package into the composition. */\nexport interface PackageAttribution {\n /** `major` for a layer package, `domain` for a plugin a domain carries. */\n kind: 'major' | 'domain';\n /** The major mode name, or the domain name. */\n mode: string;\n /** The layer the package was listed under; absent for a domain plugin. */\n layer?: string;\n}\n\nimport type { ContextPromptStage } from '../../types/contextApi';\nimport { type CountTokens, type ToolEntry, type ToolSource, tokensForTool } from '../toolInventory';\n\n/**\n * What the session is carrying, and what carrying it costs.\n *\n * Grouped by the mode that admitted each package rather than by where the tool\n * came from, because a mode is the thing a reader can switch off. The mechanism\n * (extension, MCP server, plugin) orders rows inside a group instead, so the\n * expensive thing is still easy to find.\n *\n * Schemas never travel. Only names, kinds, owners and integer counts do, which\n * keeps the payload small enough to republish whenever the composition changes.\n */\nexport const CONTEXT_PROJECTION_VERSION = 1;\n\n/** Journal entry the cockpit reads; mirrored by CONTEXT_ENTRY_TYPE in po-web. */\nexport const PO_CONTEXT_ENTRY_TYPE = 'po-context';\n\n/** Session selection and transition journal identities, not transport channels. */\nexport const PO_CONFIG_ENTRY_TYPE = 'po:config:v1';\nexport const PO_CONFIG_TRANSITION_ENTRY_TYPE = 'po:transition:v1';\n\n/** The tokenizer these counts came from, so a reader can judge them. */\nexport const CONTEXT_PROJECTION_ESTIMATOR = 'gpt-tokenizer';\n\nexport type ContextGroupKind = 'major' | 'minor' | 'domain' | 'shared' | 'core';\nexport type ContextItemSource = 'extension' | 'mcp' | 'plugin' | 'core';\n\n/** Everything the runtime could not attribute lands here rather than vanishing. */\nconst CORE_GROUP = 'core';\n/** Pi's own tools have no package; the runtime itself is the owner. */\nconst CORE_OWNER = 'pi';\nconst MCP_KEY_PREFIX = 'mcp:';\n\nconst KIND_ORDER: Record<ContextGroupKind, number> = { major: 0, minor: 1, domain: 2, shared: 3, core: 4 };\nconst SOURCE_ORDER: Record<ContextItemSource, number> = { extension: 0, mcp: 1, plugin: 2, core: 3 };\n\nexport interface ContextItemProjection {\n readonly name: string;\n readonly itemKind: 'tool' | 'skill';\n readonly source: ContextItemSource;\n /** The package, server, or plugin that registered it. */\n readonly owner: string;\n readonly tokens: number;\n readonly active: boolean;\n}\n\nexport interface ContextGroupProjection {\n readonly id: string;\n readonly label: string;\n readonly kind: ContextGroupKind;\n readonly items: readonly ContextItemProjection[];\n /** What the group costs now: active items only. */\n readonly tokens: number;\n /** What its gated items would add if switched on. */\n readonly inactiveTokens: number;\n}\n\n/**\n * What the assembled system prompt costs, and how complete the figure is.\n *\n * Kept out of `groups` on purpose. Every tool's prose and every skill's listing\n * already count inside the group subtotals, and those are slices of this same\n * text, so folding it into `totalTokens` would bill them twice.\n */\nexport interface ContextSystemPromptProjection {\n readonly tokens: number;\n readonly stage: ContextPromptStage;\n}\n\nexport interface ContextProjection {\n readonly version: typeof CONTEXT_PROJECTION_VERSION;\n readonly revision: number;\n /** Selection snapshot used to build this projection, replayable after reconnect. */\n readonly selection: {\n readonly majorMode: string;\n readonly domains: readonly string[];\n readonly profile?: string;\n };\n readonly groups: readonly ContextGroupProjection[];\n readonly totalTokens: number;\n readonly inactiveTokens: number;\n readonly systemPrompt?: ContextSystemPromptProjection;\n readonly estimator: typeof CONTEXT_PROJECTION_ESTIMATOR;\n}\n\nexport interface ContextConditionalAttribution {\n readonly kind: 'minor' | 'domain' | 'shared';\n readonly mode: string;\n readonly label?: string;\n}\n\nexport interface ContextToolInventory extends ToolEntry {\n /** A contribution-level gate is more specific than its package's major-mode owner. */\n readonly contextAttribution?: ContextConditionalAttribution;\n}\n\nexport interface ContextToolSource extends Omit<ToolSource, 'tools'> {\n readonly tools: readonly ContextToolInventory[];\n}\n\n/** Skill fields needed to project cost and serve detail. Virtual resources may not have a filesystem path. */\nexport interface ContextSkillInventory {\n readonly name: string;\n readonly description: string;\n readonly group: 'extensions' | 'help' | 'plugins' | 'default';\n readonly owner: string;\n readonly modelInvocable: boolean;\n readonly promptTokens?: number;\n readonly filePath?: string;\n /** The skill's instructions, when the host holds them; read only when a reader opens the row. */\n readonly body?: string;\n readonly contextAttribution?: ContextConditionalAttribution;\n}\n\nexport interface ContextProjectionInput {\n readonly revision: number;\n readonly majorMode: string;\n readonly profile?: string;\n /** Additional groups supplied by the composition owner, in display order. */\n readonly groups: readonly { readonly id: string; readonly label: string; readonly kind: ContextGroupKind }[];\n /** Domains the composition resolved under, whether or not they carried a plugin. */\n readonly domains: readonly string[];\n readonly sources: readonly ContextToolSource[];\n readonly skills: readonly ContextSkillInventory[];\n /**\n * Owner identifier to the mode that admitted it.\n *\n * Keys are package names for extensions and layer packages, and plugin or\n * server names for anything a domain carries. The caller owns this join\n * because only it knows how a given deployment resolved its plugins.\n */\n readonly attribution: Readonly<Record<string, PackageAttribution>>;\n readonly countTokens: CountTokens;\n /** Absent until the session has a prompt to report. */\n readonly systemPrompt?: ContextSystemPromptProjection;\n}\n\ninterface Bucket {\n kind: ContextGroupKind;\n label: string;\n items: ContextItemProjection[];\n}\n\nfunction skillSource(group: ContextSkillInventory['group']): ContextItemSource {\n if (group === 'plugins') return 'plugin';\n if (group === 'extensions') return 'extension';\n return 'core';\n}\n\nfunction byRow(left: ContextItemProjection, right: ContextItemProjection): number {\n return SOURCE_ORDER[left.source] - SOURCE_ORDER[right.source] || left.name.localeCompare(right.name);\n}\n\nexport function projectContext(input: ContextProjectionInput): ContextProjection {\n const buckets = new Map<string, Bucket>();\n\n // Declared before anything is placed, so a mode with nothing attributed to it\n // still appears. An empty group is a fact worth showing: it says the mode is\n // on and cheap, not that the runtime forgot about it.\n const declare = (id: string, label: string, kind: ContextGroupKind): void => {\n if (!buckets.has(id)) buckets.set(id, { kind, label, items: [] });\n };\n if (input.majorMode) declare(input.majorMode, input.majorMode, 'major');\n for (const group of input.groups) declare(group.id, group.label, group.kind);\n // A domain that contributed no plugin is still part of what the session is\n // running under, and a reader comparing the panel against the footer would\n // otherwise read its absence as the panel being out of date.\n for (const domain of input.domains) declare(domain, domain, 'domain');\n\n const place = (owner: string, item: ContextItemProjection, conditional?: ContextConditionalAttribution): void => {\n const attributed = conditional ?? input.attribution[owner];\n const id = attributed?.mode ?? CORE_GROUP;\n const kind = attributed?.kind ?? 'core';\n declare(id, attributed && 'label' in attributed ? (attributed.label ?? id) : id, kind);\n buckets.get(id)?.items.push(item);\n };\n\n for (const source of input.sources) {\n // An extension is owned by its package, an MCP tool by the server that\n // advertised it. `label` is display text like `scaffold · mcp`, so the\n // identifier comes from the key instead: an owner is joined on, not read.\n const owner =\n source.kind === 'extension'\n ? (source.packageName ?? source.key)\n : source.kind === 'mcp'\n ? source.key.slice(MCP_KEY_PREFIX.length)\n : CORE_OWNER;\n for (const tool of source.tools) {\n const cost = tokensForTool(tool, input.countTokens);\n const item: ContextItemProjection = {\n name: tool.name,\n itemKind: 'tool',\n source: source.kind === 'core' ? 'core' : source.kind,\n owner,\n tokens: cost.totalTokens,\n active: tool.active,\n };\n if (source.kind === 'core') {\n declare(CORE_GROUP, CORE_GROUP, 'core');\n buckets.get(CORE_GROUP)?.items.push(item);\n } else {\n place(owner, item, tool.contextAttribution);\n }\n }\n }\n\n for (const skill of input.skills) {\n place(\n skill.owner,\n {\n name: skill.name,\n itemKind: 'skill',\n source: skillSource(skill.group),\n owner: skill.owner,\n tokens: skill.promptTokens ?? 0,\n // A skill is always offered to the model; there is no inactive state for\n // it the way there is for a tool Pi has filtered out.\n active: true,\n },\n skill.contextAttribution,\n );\n }\n\n const groups: ContextGroupProjection[] = [...buckets.entries()]\n .map(([id, bucket]) => {\n const items = [...bucket.items].sort(byRow);\n // Only what is actually sent counts. Pi builds the prompt from its\n // active tools, so a gated one costs nothing until it is switched on,\n // and folding it into the total would overstate the bill by a third.\n return {\n id,\n label: bucket.label,\n kind: bucket.kind,\n items,\n tokens: items.reduce((total, item) => total + (item.active ? item.tokens : 0), 0),\n inactiveTokens: items.reduce((total, item) => total + (item.active ? 0 : item.tokens), 0),\n };\n })\n .sort((left, right) => KIND_ORDER[left.kind] - KIND_ORDER[right.kind] || left.label.localeCompare(right.label));\n\n return {\n version: CONTEXT_PROJECTION_VERSION,\n revision: input.revision,\n selection: {\n majorMode: input.majorMode,\n domains: [...input.domains],\n ...(input.profile === undefined ? {} : { profile: input.profile }),\n },\n groups,\n totalTokens: groups.reduce((total, group) => total + group.tokens, 0),\n inactiveTokens: groups.reduce((total, group) => total + group.inactiveTokens, 0),\n ...(input.systemPrompt === undefined ? {} : { systemPrompt: input.systemPrompt }),\n estimator: CONTEXT_PROJECTION_ESTIMATOR,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"contextProjection.cjs","names":["tokensForTool"],"sources":["../src/services/contextProjection/index.ts"],"sourcesContent":["/** What admitted a package into the composition. */\nexport interface PackageAttribution {\n /** `major` for a layer package, `domain` for a plugin a domain carries. */\n kind: 'major' | 'domain';\n /** The major mode name, or the domain name. */\n mode: string;\n /** The layer the package was listed under; absent for a domain plugin. */\n layer?: string;\n}\n\nimport type { ContextPromptStage } from '../../types/contextApi';\nimport { type CountTokens, type ToolEntry, type ToolSource, tokensForTool } from '../toolInventory';\n\n/**\n * What the session is carrying, and what carrying it costs.\n *\n * Grouped by the mode that admitted each package rather than by where the tool\n * came from, because a mode is the thing a reader can switch off. The mechanism\n * (extension, MCP server, plugin) orders rows inside a group instead, so the\n * expensive thing is still easy to find.\n *\n * Schemas never travel. Only names, kinds, owners and integer counts do, which\n * keeps the payload small enough to republish whenever the composition changes.\n */\nexport const CONTEXT_PROJECTION_VERSION = 1;\n\n/** Journal entry the cockpit reads; mirrored by CONTEXT_ENTRY_TYPE in po-web. */\nexport const PO_CONTEXT_ENTRY_TYPE = 'po-context';\n\n/** Session selection and transition journal identities, not transport channels. */\nexport const PO_CONFIG_ENTRY_TYPE = 'po:config:v1';\nexport const PO_CONFIG_TRANSITION_ENTRY_TYPE = 'po:transition:v1';\n\n/** The tokenizer these counts came from, so a reader can judge them. */\nexport const CONTEXT_PROJECTION_ESTIMATOR = 'gpt-tokenizer';\n\nexport type ContextGroupKind = 'major' | 'minor' | 'domain' | 'shared' | 'core';\nexport type ContextItemSource = 'extension' | 'mcp' | 'plugin' | 'core';\n\n/** Everything the runtime could not attribute lands here rather than vanishing. */\nconst CORE_GROUP = 'core';\n/** Pi's own tools have no package; the runtime itself is the owner. */\nconst CORE_OWNER = 'pi';\nconst MCP_KEY_PREFIX = 'mcp:';\n\nconst KIND_ORDER: Record<ContextGroupKind, number> = { major: 0, minor: 1, domain: 2, shared: 3, core: 4 };\nconst SOURCE_ORDER: Record<ContextItemSource, number> = { extension: 0, mcp: 1, plugin: 2, core: 3 };\n\nexport interface ContextItemProjection {\n readonly name: string;\n readonly itemKind: 'tool' | 'skill';\n readonly source: ContextItemSource;\n /** The package, server, or plugin that registered it. */\n readonly owner: string;\n readonly tokens: number;\n readonly active: boolean;\n}\n\nexport interface ContextGroupProjection {\n readonly id: string;\n readonly label: string;\n readonly kind: ContextGroupKind;\n readonly items: readonly ContextItemProjection[];\n /** What the group costs now: active items only. */\n readonly tokens: number;\n /** What its gated items would add if switched on. */\n readonly inactiveTokens: number;\n}\n\n/**\n * What the assembled system prompt costs, and how complete the figure is.\n *\n * Kept out of `groups` on purpose. Every tool's prose and every skill's listing\n * already count inside the group subtotals, and those are slices of this same\n * text, so folding it into `totalTokens` would bill them twice.\n */\nexport interface ContextSystemPromptProjection {\n readonly tokens: number;\n readonly stage: ContextPromptStage;\n}\n\nexport interface ContextProjection {\n readonly version: typeof CONTEXT_PROJECTION_VERSION;\n readonly revision: number;\n /** Selection snapshot used to build this projection, replayable after reconnect. */\n readonly selection: {\n readonly majorMode: string;\n readonly domains: readonly string[];\n readonly profile?: string;\n /** Requested profile when none is active; null records an explicit None pick. Absent on legacy records. */\n readonly requestedProfile?: string | null;\n };\n readonly groups: readonly ContextGroupProjection[];\n readonly totalTokens: number;\n readonly inactiveTokens: number;\n readonly systemPrompt?: ContextSystemPromptProjection;\n readonly estimator: typeof CONTEXT_PROJECTION_ESTIMATOR;\n}\n\nexport interface ContextConditionalAttribution {\n readonly kind: 'minor' | 'domain' | 'shared';\n readonly mode: string;\n readonly label?: string;\n}\n\nexport interface ContextToolInventory extends ToolEntry {\n /** A contribution-level gate is more specific than its package's major-mode owner. */\n readonly contextAttribution?: ContextConditionalAttribution;\n}\n\nexport interface ContextToolSource extends Omit<ToolSource, 'tools'> {\n readonly tools: readonly ContextToolInventory[];\n}\n\n/** Skill fields needed to project cost and serve detail. Virtual resources may not have a filesystem path. */\nexport interface ContextSkillInventory {\n readonly name: string;\n readonly description: string;\n readonly group: 'extensions' | 'help' | 'plugins' | 'default';\n readonly owner: string;\n readonly modelInvocable: boolean;\n readonly promptTokens?: number;\n readonly filePath?: string;\n /** The skill's instructions, when the host holds them; read only when a reader opens the row. */\n readonly body?: string;\n readonly contextAttribution?: ContextConditionalAttribution;\n}\n\nexport interface ContextProjectionInput {\n readonly revision: number;\n readonly majorMode: string;\n readonly profile?: string;\n readonly requestedProfile?: string | null;\n /** Additional groups supplied by the composition owner, in display order. */\n readonly groups: readonly { readonly id: string; readonly label: string; readonly kind: ContextGroupKind }[];\n /** Domains the composition resolved under, whether or not they carried a plugin. */\n readonly domains: readonly string[];\n readonly sources: readonly ContextToolSource[];\n readonly skills: readonly ContextSkillInventory[];\n /**\n * Owner identifier to the mode that admitted it.\n *\n * Keys are package names for extensions and layer packages, and plugin or\n * server names for anything a domain carries. The caller owns this join\n * because only it knows how a given deployment resolved its plugins.\n */\n readonly attribution: Readonly<Record<string, PackageAttribution>>;\n readonly countTokens: CountTokens;\n /** Absent until the session has a prompt to report. */\n readonly systemPrompt?: ContextSystemPromptProjection;\n}\n\n/** Reads only replayable selection fields, accepting legacy projections without a request marker. */\nexport function readProjectedSelection(data: unknown): ContextProjection['selection'] | undefined {\n if (typeof data !== 'object' || data === null || Array.isArray(data) || !('selection' in data)) return undefined;\n const selection = data.selection;\n if (typeof selection !== 'object' || selection === null || Array.isArray(selection)) return undefined;\n if (!('majorMode' in selection) || typeof selection.majorMode !== 'string') return undefined;\n if (\n !('domains' in selection) ||\n !Array.isArray(selection.domains) ||\n !selection.domains.every((domain) => typeof domain === 'string')\n )\n return undefined;\n const profile = 'profile' in selection ? selection.profile : undefined;\n if (profile !== undefined && typeof profile !== 'string') return undefined;\n const requestedProfile = 'requestedProfile' in selection ? selection.requestedProfile : undefined;\n return {\n majorMode: selection.majorMode,\n domains: [...selection.domains],\n profile,\n ...(typeof requestedProfile === 'string' || requestedProfile === null ? { requestedProfile } : {}),\n };\n}\n\ninterface Bucket {\n kind: ContextGroupKind;\n label: string;\n items: ContextItemProjection[];\n}\n\nfunction skillSource(group: ContextSkillInventory['group']): ContextItemSource {\n if (group === 'plugins') return 'plugin';\n if (group === 'extensions') return 'extension';\n return 'core';\n}\n\nfunction byRow(left: ContextItemProjection, right: ContextItemProjection): number {\n return SOURCE_ORDER[left.source] - SOURCE_ORDER[right.source] || left.name.localeCompare(right.name);\n}\n\nexport function projectContext(input: ContextProjectionInput): ContextProjection {\n const buckets = new Map<string, Bucket>();\n\n // Declared before anything is placed, so a mode with nothing attributed to it\n // still appears. An empty group is a fact worth showing: it says the mode is\n // on and cheap, not that the runtime forgot about it.\n const declare = (id: string, label: string, kind: ContextGroupKind): void => {\n if (!buckets.has(id)) buckets.set(id, { kind, label, items: [] });\n };\n if (input.majorMode) declare(input.majorMode, input.majorMode, 'major');\n for (const group of input.groups) declare(group.id, group.label, group.kind);\n // A domain that contributed no plugin is still part of what the session is\n // running under, and a reader comparing the panel against the footer would\n // otherwise read its absence as the panel being out of date.\n for (const domain of input.domains) declare(domain, domain, 'domain');\n\n const place = (owner: string, item: ContextItemProjection, conditional?: ContextConditionalAttribution): void => {\n const attributed = conditional ?? input.attribution[owner];\n const id = attributed?.mode ?? CORE_GROUP;\n const kind = attributed?.kind ?? 'core';\n declare(id, attributed && 'label' in attributed ? (attributed.label ?? id) : id, kind);\n buckets.get(id)?.items.push(item);\n };\n\n for (const source of input.sources) {\n // An extension is owned by its package, an MCP tool by the server that\n // advertised it. `label` is display text like `scaffold · mcp`, so the\n // identifier comes from the key instead: an owner is joined on, not read.\n const owner =\n source.kind === 'extension'\n ? (source.packageName ?? source.key)\n : source.kind === 'mcp'\n ? source.key.slice(MCP_KEY_PREFIX.length)\n : CORE_OWNER;\n for (const tool of source.tools) {\n const cost = tokensForTool(tool, input.countTokens);\n const item: ContextItemProjection = {\n name: tool.name,\n itemKind: 'tool',\n source: source.kind === 'core' ? 'core' : source.kind,\n owner,\n tokens: cost.totalTokens,\n active: tool.active,\n };\n if (source.kind === 'core') {\n declare(CORE_GROUP, CORE_GROUP, 'core');\n buckets.get(CORE_GROUP)?.items.push(item);\n } else {\n place(owner, item, tool.contextAttribution);\n }\n }\n }\n\n for (const skill of input.skills) {\n place(\n skill.owner,\n {\n name: skill.name,\n itemKind: 'skill',\n source: skillSource(skill.group),\n owner: skill.owner,\n tokens: skill.promptTokens ?? 0,\n // A skill is always offered to the model; there is no inactive state for\n // it the way there is for a tool Pi has filtered out.\n active: true,\n },\n skill.contextAttribution,\n );\n }\n\n const groups: ContextGroupProjection[] = [...buckets.entries()]\n .map(([id, bucket]) => {\n const items = [...bucket.items].sort(byRow);\n // Only what is actually sent counts. Pi builds the prompt from its\n // active tools, so a gated one costs nothing until it is switched on,\n // and folding it into the total would overstate the bill by a third.\n return {\n id,\n label: bucket.label,\n kind: bucket.kind,\n items,\n tokens: items.reduce((total, item) => total + (item.active ? item.tokens : 0), 0),\n inactiveTokens: items.reduce((total, item) => total + (item.active ? 0 : item.tokens), 0),\n };\n })\n .sort((left, right) => KIND_ORDER[left.kind] - KIND_ORDER[right.kind] || left.label.localeCompare(right.label));\n\n return {\n version: CONTEXT_PROJECTION_VERSION,\n revision: input.revision,\n selection: {\n majorMode: input.majorMode,\n domains: [...input.domains],\n ...(input.profile === undefined ? {} : { profile: input.profile }),\n ...(input.requestedProfile === undefined ? {} : { requestedProfile: input.requestedProfile }),\n },\n groups,\n totalTokens: groups.reduce((total, group) => total + group.tokens, 0),\n inactiveTokens: groups.reduce((total, group) => total + group.inactiveTokens, 0),\n ...(input.systemPrompt === undefined ? {} : { systemPrompt: input.systemPrompt }),\n estimator: CONTEXT_PROJECTION_ESTIMATOR,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;AAwBA,MAAa,6BAA6B;;AAG1C,MAAa,wBAAwB;;AAGrC,MAAa,uBAAuB;AACpC,MAAa,kCAAkC;;AAG/C,MAAa,+BAA+B;;AAM5C,MAAM,aAAa;;AAEnB,MAAM,aAAa;AAGnB,MAAM,aAA+C;CAAE,OAAO;CAAG,OAAO;CAAG,QAAQ;CAAG,QAAQ;CAAG,MAAM;AAAE;AACzG,MAAM,eAAkD;CAAE,WAAW;CAAG,KAAK;CAAG,QAAQ;CAAG,MAAM;AAAE;;AA2GnG,SAAgB,uBAAuB,MAA2D;CAChG,IAAI,OAAO,SAAS,YAAY,SAAS,QAAQ,MAAM,QAAQ,IAAI,KAAK,EAAE,eAAe,OAAO,OAAO,KAAA;CACvG,MAAM,YAAY,KAAK;CACvB,IAAI,OAAO,cAAc,YAAY,cAAc,QAAQ,MAAM,QAAQ,SAAS,GAAG,OAAO,KAAA;CAC5F,IAAI,EAAE,eAAe,cAAc,OAAO,UAAU,cAAc,UAAU,OAAO,KAAA;CACnF,IACE,EAAE,aAAa,cACf,CAAC,MAAM,QAAQ,UAAU,OAAO,KAChC,CAAC,UAAU,QAAQ,OAAO,WAAW,OAAO,WAAW,QAAQ,GAE/D,OAAO,KAAA;CACT,MAAM,UAAU,aAAa,YAAY,UAAU,UAAU,KAAA;CAC7D,IAAI,YAAY,KAAA,KAAa,OAAO,YAAY,UAAU,OAAO,KAAA;CACjE,MAAM,mBAAmB,sBAAsB,YAAY,UAAU,mBAAmB,KAAA;CACxF,OAAO;EACL,WAAW,UAAU;EACrB,SAAS,CAAC,GAAG,UAAU,OAAO;EAC9B;EACA,GAAI,OAAO,qBAAqB,YAAY,qBAAqB,OAAO,EAAE,iBAAiB,IAAI,CAAC;CAClG;AACF;AAQA,SAAS,YAAY,OAA0D;CAC7E,IAAI,UAAU,WAAW,OAAO;CAChC,IAAI,UAAU,cAAc,OAAO;CACnC,OAAO;AACT;AAEA,SAAS,MAAM,MAA6B,OAAsC;CAChF,OAAO,aAAa,KAAK,UAAU,aAAa,MAAM,WAAW,KAAK,KAAK,cAAc,MAAM,IAAI;AACrG;AAEA,SAAgB,eAAe,OAAkD;CAC/E,MAAM,0BAAU,IAAI,IAAoB;CAKxC,MAAM,WAAW,IAAY,OAAe,SAAiC;EAC3E,IAAI,CAAC,QAAQ,IAAI,EAAE,GAAG,QAAQ,IAAI,IAAI;GAAE;GAAM;GAAO,OAAO,CAAC;EAAE,CAAC;CAClE;CACA,IAAI,MAAM,WAAW,QAAQ,MAAM,WAAW,MAAM,WAAW,OAAO;CACtE,KAAK,MAAM,SAAS,MAAM,QAAQ,QAAQ,MAAM,IAAI,MAAM,OAAO,MAAM,IAAI;CAI3E,KAAK,MAAM,UAAU,MAAM,SAAS,QAAQ,QAAQ,QAAQ,QAAQ;CAEpE,MAAM,SAAS,OAAe,MAA6B,gBAAsD;EAC/G,MAAM,aAAa,eAAe,MAAM,YAAY;EACpD,MAAM,KAAK,YAAY,QAAQ;EAC/B,MAAM,OAAO,YAAY,QAAQ;EACjC,QAAQ,IAAI,cAAc,WAAW,aAAc,WAAW,SAAS,KAAM,IAAI,IAAI;EACrF,QAAQ,IAAI,EAAE,CAAC,EAAE,MAAM,KAAK,IAAI;CAClC;CAEA,KAAK,MAAM,UAAU,MAAM,SAAS;EAIlC,MAAM,QACJ,OAAO,SAAS,cACX,OAAO,eAAe,OAAO,MAC9B,OAAO,SAAS,QACd,OAAO,IAAI,MAAM,CAAqB,IACtC;EACR,KAAK,MAAM,QAAQ,OAAO,OAAO;GAC/B,MAAM,OAAOA,cAAAA,cAAc,MAAM,MAAM,WAAW;GAClD,MAAM,OAA8B;IAClC,MAAM,KAAK;IACX,UAAU;IACV,QAAQ,OAAO,SAAS,SAAS,SAAS,OAAO;IACjD;IACA,QAAQ,KAAK;IACb,QAAQ,KAAK;GACf;GACA,IAAI,OAAO,SAAS,QAAQ;IAC1B,QAAQ,YAAY,YAAY,MAAM;IACtC,QAAQ,IAAI,UAAU,CAAC,EAAE,MAAM,KAAK,IAAI;GAC1C,OACE,MAAM,OAAO,MAAM,KAAK,kBAAkB;EAE9C;CACF;CAEA,KAAK,MAAM,SAAS,MAAM,QACxB,MACE,MAAM,OACN;EACE,MAAM,MAAM;EACZ,UAAU;EACV,QAAQ,YAAY,MAAM,KAAK;EAC/B,OAAO,MAAM;EACb,QAAQ,MAAM,gBAAgB;EAG9B,QAAQ;CACV,GACA,MAAM,kBACR;CAGF,MAAM,SAAmC,CAAC,GAAG,QAAQ,QAAQ,CAAC,CAAC,CAC5D,KAAK,CAAC,IAAI,YAAY;EACrB,MAAM,QAAQ,CAAC,GAAG,OAAO,KAAK,CAAC,CAAC,KAAK,KAAK;EAI1C,OAAO;GACL;GACA,OAAO,OAAO;GACd,MAAM,OAAO;GACb;GACA,QAAQ,MAAM,QAAQ,OAAO,SAAS,SAAS,KAAK,SAAS,KAAK,SAAS,IAAI,CAAC;GAChF,gBAAgB,MAAM,QAAQ,OAAO,SAAS,SAAS,KAAK,SAAS,IAAI,KAAK,SAAS,CAAC;EAC1F;CACF,CAAC,CAAC,CACD,MAAM,MAAM,UAAU,WAAW,KAAK,QAAQ,WAAW,MAAM,SAAS,KAAK,MAAM,cAAc,MAAM,KAAK,CAAC;CAEhH,OAAO;EACL,SAAA;EACA,UAAU,MAAM;EAChB,WAAW;GACT,WAAW,MAAM;GACjB,SAAS,CAAC,GAAG,MAAM,OAAO;GAC1B,GAAI,MAAM,YAAY,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS,MAAM,QAAQ;GAChE,GAAI,MAAM,qBAAqB,KAAA,IAAY,CAAC,IAAI,EAAE,kBAAkB,MAAM,iBAAiB;EAC7F;EACA;EACA,aAAa,OAAO,QAAQ,OAAO,UAAU,QAAQ,MAAM,QAAQ,CAAC;EACpE,gBAAgB,OAAO,QAAQ,OAAO,UAAU,QAAQ,MAAM,gBAAgB,CAAC;EAC/E,GAAI,MAAM,iBAAiB,KAAA,IAAY,CAAC,IAAI,EAAE,cAAc,MAAM,aAAa;EAC/E,WAAW;CACb;AACF"}
@@ -70,6 +70,8 @@ export interface ContextProjection {
70
70
  readonly majorMode: string;
71
71
  readonly domains: readonly string[];
72
72
  readonly profile?: string;
73
+ /** Requested profile when none is active; null records an explicit None pick. Absent on legacy records. */
74
+ readonly requestedProfile?: string | null;
73
75
  };
74
76
  readonly groups: readonly ContextGroupProjection[];
75
77
  readonly totalTokens: number;
@@ -106,6 +108,7 @@ export interface ContextProjectionInput {
106
108
  readonly revision: number;
107
109
  readonly majorMode: string;
108
110
  readonly profile?: string;
111
+ readonly requestedProfile?: string | null;
109
112
  /** Additional groups supplied by the composition owner, in display order. */
110
113
  readonly groups: readonly {
111
114
  readonly id: string;
@@ -128,6 +131,8 @@ export interface ContextProjectionInput {
128
131
  /** Absent until the session has a prompt to report. */
129
132
  readonly systemPrompt?: ContextSystemPromptProjection;
130
133
  }
134
+ /** Reads only replayable selection fields, accepting legacy projections without a request marker. */
135
+ export declare function readProjectedSelection(data: unknown): ContextProjection['selection'] | undefined;
131
136
  export declare function projectContext(input: ContextProjectionInput): ContextProjection;
132
137
  //#endregion
133
138
  //# sourceMappingURL=contextProjection.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"contextProjection.d.mts","names":[],"sources":["../src/services/contextProjection/index.ts"],"mappings":";;;;;iBACiB;;EAEf;;EAEA;;EAEA;;;;;;;;;;;;;qBAiBW;;qBAGA;;qBAGA;qBACA;;qBAGA;YAED;YACA;iBAWK;WACN;WACA;WACA,QAAQ;;WAER;WACA;WACA;;iBAGM;WACN;WACA;WACA,MAAM;WACN,gBAAgB;;WAEhB;;WAEA;;;;;;;;;iBAUM;WACN;WACA,OAAO;;iBAGD;WACN,gBAAgB;WAChB;;WAEA;aACE;aACA;aACA;;WAEF,iBAAiB;WACjB;WACA;WACA,eAAe;WACf,kBAAkB;;iBAGZ;WACN;WACA;WACA;;iBAGM,6BAA6B;;WAEnC,qBAAqB;;iBAGf,0BAA0B,KAAK;WACrC,gBAAgB;;;iBAIV;WACN;WACA;WACA;WACA;WACA;WACA;WACA;;WAEA;WACA,qBAAqB;;iBAGf;WACN;WACA;WACA;;WAEA;aAA4B;aAAqB;aAAwB,MAAM;;;WAE/E;WACA,kBAAkB;WAClB,iBAAiB;;;;;;;;WAQjB,aAAa,SAAS,eAAe;WACrC,aAAa;;WAEb,eAAe;;wBAmBV,eAAe,OAAO,yBAAyB"}
1
+ {"version":3,"file":"contextProjection.d.mts","names":[],"sources":["../src/services/contextProjection/index.ts"],"mappings":";;;;;iBACiB;;EAEf;;EAEA;;EAEA;;;;;;;;;;;;;qBAiBW;;qBAGA;;qBAGA;qBACA;;qBAGA;YAED;YACA;iBAWK;WACN;WACA;WACA,QAAQ;;WAER;WACA;WACA;;iBAGM;WACN;WACA;WACA,MAAM;WACN,gBAAgB;;WAEhB;;WAEA;;;;;;;;;iBAUM;WACN;WACA,OAAO;;iBAGD;WACN,gBAAgB;WAChB;;WAEA;aACE;aACA;aACA;;aAEA;;WAEF,iBAAiB;WACjB;WACA;WACA,eAAe;WACf,kBAAkB;;iBAGZ;WACN;WACA;WACA;;iBAGM,6BAA6B;;WAEnC,qBAAqB;;iBAGf,0BAA0B,KAAK;WACrC,gBAAgB;;;iBAIV;WACN;WACA;WACA;WACA;WACA;WACA;WACA;;WAEA;WACA,qBAAqB;;iBAGf;WACN;WACA;WACA;WACA;;WAEA;aAA4B;aAAqB;aAAwB,MAAM;;;WAE/E;WACA,kBAAkB;WAClB,iBAAiB;;;;;;;;WAQjB,aAAa,SAAS,eAAe;WACrC,aAAa;;WAEb,eAAe;;;wBAIV,uBAAuB,gBAAgB;wBAsCvC,eAAe,OAAO,yBAAyB"}
@@ -36,6 +36,23 @@ const SOURCE_ORDER = {
36
36
  plugin: 2,
37
37
  core: 3
38
38
  };
39
+ /** Reads only replayable selection fields, accepting legacy projections without a request marker. */
40
+ function readProjectedSelection(data) {
41
+ if (typeof data !== "object" || data === null || Array.isArray(data) || !("selection" in data)) return void 0;
42
+ const selection = data.selection;
43
+ if (typeof selection !== "object" || selection === null || Array.isArray(selection)) return void 0;
44
+ if (!("majorMode" in selection) || typeof selection.majorMode !== "string") return void 0;
45
+ if (!("domains" in selection) || !Array.isArray(selection.domains) || !selection.domains.every((domain) => typeof domain === "string")) return void 0;
46
+ const profile = "profile" in selection ? selection.profile : void 0;
47
+ if (profile !== void 0 && typeof profile !== "string") return void 0;
48
+ const requestedProfile = "requestedProfile" in selection ? selection.requestedProfile : void 0;
49
+ return {
50
+ majorMode: selection.majorMode,
51
+ domains: [...selection.domains],
52
+ profile,
53
+ ...typeof requestedProfile === "string" || requestedProfile === null ? { requestedProfile } : {}
54
+ };
55
+ }
39
56
  function skillSource(group) {
40
57
  if (group === "plugins") return "plugin";
41
58
  if (group === "extensions") return "extension";
@@ -106,7 +123,8 @@ function projectContext(input) {
106
123
  selection: {
107
124
  majorMode: input.majorMode,
108
125
  domains: [...input.domains],
109
- ...input.profile === void 0 ? {} : { profile: input.profile }
126
+ ...input.profile === void 0 ? {} : { profile: input.profile },
127
+ ...input.requestedProfile === void 0 ? {} : { requestedProfile: input.requestedProfile }
110
128
  },
111
129
  groups,
112
130
  totalTokens: groups.reduce((total, group) => total + group.tokens, 0),
@@ -116,6 +134,6 @@ function projectContext(input) {
116
134
  };
117
135
  }
118
136
  //#endregion
119
- export { CONTEXT_PROJECTION_ESTIMATOR, CONTEXT_PROJECTION_VERSION, PO_CONFIG_ENTRY_TYPE, PO_CONFIG_TRANSITION_ENTRY_TYPE, PO_CONTEXT_ENTRY_TYPE, projectContext };
137
+ export { CONTEXT_PROJECTION_ESTIMATOR, CONTEXT_PROJECTION_VERSION, PO_CONFIG_ENTRY_TYPE, PO_CONFIG_TRANSITION_ENTRY_TYPE, PO_CONTEXT_ENTRY_TYPE, projectContext, readProjectedSelection };
120
138
 
121
139
  //# sourceMappingURL=contextProjection.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"contextProjection.mjs","names":[],"sources":["../src/services/contextProjection/index.ts"],"sourcesContent":["/** What admitted a package into the composition. */\nexport interface PackageAttribution {\n /** `major` for a layer package, `domain` for a plugin a domain carries. */\n kind: 'major' | 'domain';\n /** The major mode name, or the domain name. */\n mode: string;\n /** The layer the package was listed under; absent for a domain plugin. */\n layer?: string;\n}\n\nimport type { ContextPromptStage } from '../../types/contextApi';\nimport { type CountTokens, type ToolEntry, type ToolSource, tokensForTool } from '../toolInventory';\n\n/**\n * What the session is carrying, and what carrying it costs.\n *\n * Grouped by the mode that admitted each package rather than by where the tool\n * came from, because a mode is the thing a reader can switch off. The mechanism\n * (extension, MCP server, plugin) orders rows inside a group instead, so the\n * expensive thing is still easy to find.\n *\n * Schemas never travel. Only names, kinds, owners and integer counts do, which\n * keeps the payload small enough to republish whenever the composition changes.\n */\nexport const CONTEXT_PROJECTION_VERSION = 1;\n\n/** Journal entry the cockpit reads; mirrored by CONTEXT_ENTRY_TYPE in po-web. */\nexport const PO_CONTEXT_ENTRY_TYPE = 'po-context';\n\n/** Session selection and transition journal identities, not transport channels. */\nexport const PO_CONFIG_ENTRY_TYPE = 'po:config:v1';\nexport const PO_CONFIG_TRANSITION_ENTRY_TYPE = 'po:transition:v1';\n\n/** The tokenizer these counts came from, so a reader can judge them. */\nexport const CONTEXT_PROJECTION_ESTIMATOR = 'gpt-tokenizer';\n\nexport type ContextGroupKind = 'major' | 'minor' | 'domain' | 'shared' | 'core';\nexport type ContextItemSource = 'extension' | 'mcp' | 'plugin' | 'core';\n\n/** Everything the runtime could not attribute lands here rather than vanishing. */\nconst CORE_GROUP = 'core';\n/** Pi's own tools have no package; the runtime itself is the owner. */\nconst CORE_OWNER = 'pi';\nconst MCP_KEY_PREFIX = 'mcp:';\n\nconst KIND_ORDER: Record<ContextGroupKind, number> = { major: 0, minor: 1, domain: 2, shared: 3, core: 4 };\nconst SOURCE_ORDER: Record<ContextItemSource, number> = { extension: 0, mcp: 1, plugin: 2, core: 3 };\n\nexport interface ContextItemProjection {\n readonly name: string;\n readonly itemKind: 'tool' | 'skill';\n readonly source: ContextItemSource;\n /** The package, server, or plugin that registered it. */\n readonly owner: string;\n readonly tokens: number;\n readonly active: boolean;\n}\n\nexport interface ContextGroupProjection {\n readonly id: string;\n readonly label: string;\n readonly kind: ContextGroupKind;\n readonly items: readonly ContextItemProjection[];\n /** What the group costs now: active items only. */\n readonly tokens: number;\n /** What its gated items would add if switched on. */\n readonly inactiveTokens: number;\n}\n\n/**\n * What the assembled system prompt costs, and how complete the figure is.\n *\n * Kept out of `groups` on purpose. Every tool's prose and every skill's listing\n * already count inside the group subtotals, and those are slices of this same\n * text, so folding it into `totalTokens` would bill them twice.\n */\nexport interface ContextSystemPromptProjection {\n readonly tokens: number;\n readonly stage: ContextPromptStage;\n}\n\nexport interface ContextProjection {\n readonly version: typeof CONTEXT_PROJECTION_VERSION;\n readonly revision: number;\n /** Selection snapshot used to build this projection, replayable after reconnect. */\n readonly selection: {\n readonly majorMode: string;\n readonly domains: readonly string[];\n readonly profile?: string;\n };\n readonly groups: readonly ContextGroupProjection[];\n readonly totalTokens: number;\n readonly inactiveTokens: number;\n readonly systemPrompt?: ContextSystemPromptProjection;\n readonly estimator: typeof CONTEXT_PROJECTION_ESTIMATOR;\n}\n\nexport interface ContextConditionalAttribution {\n readonly kind: 'minor' | 'domain' | 'shared';\n readonly mode: string;\n readonly label?: string;\n}\n\nexport interface ContextToolInventory extends ToolEntry {\n /** A contribution-level gate is more specific than its package's major-mode owner. */\n readonly contextAttribution?: ContextConditionalAttribution;\n}\n\nexport interface ContextToolSource extends Omit<ToolSource, 'tools'> {\n readonly tools: readonly ContextToolInventory[];\n}\n\n/** Skill fields needed to project cost and serve detail. Virtual resources may not have a filesystem path. */\nexport interface ContextSkillInventory {\n readonly name: string;\n readonly description: string;\n readonly group: 'extensions' | 'help' | 'plugins' | 'default';\n readonly owner: string;\n readonly modelInvocable: boolean;\n readonly promptTokens?: number;\n readonly filePath?: string;\n /** The skill's instructions, when the host holds them; read only when a reader opens the row. */\n readonly body?: string;\n readonly contextAttribution?: ContextConditionalAttribution;\n}\n\nexport interface ContextProjectionInput {\n readonly revision: number;\n readonly majorMode: string;\n readonly profile?: string;\n /** Additional groups supplied by the composition owner, in display order. */\n readonly groups: readonly { readonly id: string; readonly label: string; readonly kind: ContextGroupKind }[];\n /** Domains the composition resolved under, whether or not they carried a plugin. */\n readonly domains: readonly string[];\n readonly sources: readonly ContextToolSource[];\n readonly skills: readonly ContextSkillInventory[];\n /**\n * Owner identifier to the mode that admitted it.\n *\n * Keys are package names for extensions and layer packages, and plugin or\n * server names for anything a domain carries. The caller owns this join\n * because only it knows how a given deployment resolved its plugins.\n */\n readonly attribution: Readonly<Record<string, PackageAttribution>>;\n readonly countTokens: CountTokens;\n /** Absent until the session has a prompt to report. */\n readonly systemPrompt?: ContextSystemPromptProjection;\n}\n\ninterface Bucket {\n kind: ContextGroupKind;\n label: string;\n items: ContextItemProjection[];\n}\n\nfunction skillSource(group: ContextSkillInventory['group']): ContextItemSource {\n if (group === 'plugins') return 'plugin';\n if (group === 'extensions') return 'extension';\n return 'core';\n}\n\nfunction byRow(left: ContextItemProjection, right: ContextItemProjection): number {\n return SOURCE_ORDER[left.source] - SOURCE_ORDER[right.source] || left.name.localeCompare(right.name);\n}\n\nexport function projectContext(input: ContextProjectionInput): ContextProjection {\n const buckets = new Map<string, Bucket>();\n\n // Declared before anything is placed, so a mode with nothing attributed to it\n // still appears. An empty group is a fact worth showing: it says the mode is\n // on and cheap, not that the runtime forgot about it.\n const declare = (id: string, label: string, kind: ContextGroupKind): void => {\n if (!buckets.has(id)) buckets.set(id, { kind, label, items: [] });\n };\n if (input.majorMode) declare(input.majorMode, input.majorMode, 'major');\n for (const group of input.groups) declare(group.id, group.label, group.kind);\n // A domain that contributed no plugin is still part of what the session is\n // running under, and a reader comparing the panel against the footer would\n // otherwise read its absence as the panel being out of date.\n for (const domain of input.domains) declare(domain, domain, 'domain');\n\n const place = (owner: string, item: ContextItemProjection, conditional?: ContextConditionalAttribution): void => {\n const attributed = conditional ?? input.attribution[owner];\n const id = attributed?.mode ?? CORE_GROUP;\n const kind = attributed?.kind ?? 'core';\n declare(id, attributed && 'label' in attributed ? (attributed.label ?? id) : id, kind);\n buckets.get(id)?.items.push(item);\n };\n\n for (const source of input.sources) {\n // An extension is owned by its package, an MCP tool by the server that\n // advertised it. `label` is display text like `scaffold · mcp`, so the\n // identifier comes from the key instead: an owner is joined on, not read.\n const owner =\n source.kind === 'extension'\n ? (source.packageName ?? source.key)\n : source.kind === 'mcp'\n ? source.key.slice(MCP_KEY_PREFIX.length)\n : CORE_OWNER;\n for (const tool of source.tools) {\n const cost = tokensForTool(tool, input.countTokens);\n const item: ContextItemProjection = {\n name: tool.name,\n itemKind: 'tool',\n source: source.kind === 'core' ? 'core' : source.kind,\n owner,\n tokens: cost.totalTokens,\n active: tool.active,\n };\n if (source.kind === 'core') {\n declare(CORE_GROUP, CORE_GROUP, 'core');\n buckets.get(CORE_GROUP)?.items.push(item);\n } else {\n place(owner, item, tool.contextAttribution);\n }\n }\n }\n\n for (const skill of input.skills) {\n place(\n skill.owner,\n {\n name: skill.name,\n itemKind: 'skill',\n source: skillSource(skill.group),\n owner: skill.owner,\n tokens: skill.promptTokens ?? 0,\n // A skill is always offered to the model; there is no inactive state for\n // it the way there is for a tool Pi has filtered out.\n active: true,\n },\n skill.contextAttribution,\n );\n }\n\n const groups: ContextGroupProjection[] = [...buckets.entries()]\n .map(([id, bucket]) => {\n const items = [...bucket.items].sort(byRow);\n // Only what is actually sent counts. Pi builds the prompt from its\n // active tools, so a gated one costs nothing until it is switched on,\n // and folding it into the total would overstate the bill by a third.\n return {\n id,\n label: bucket.label,\n kind: bucket.kind,\n items,\n tokens: items.reduce((total, item) => total + (item.active ? item.tokens : 0), 0),\n inactiveTokens: items.reduce((total, item) => total + (item.active ? 0 : item.tokens), 0),\n };\n })\n .sort((left, right) => KIND_ORDER[left.kind] - KIND_ORDER[right.kind] || left.label.localeCompare(right.label));\n\n return {\n version: CONTEXT_PROJECTION_VERSION,\n revision: input.revision,\n selection: {\n majorMode: input.majorMode,\n domains: [...input.domains],\n ...(input.profile === undefined ? {} : { profile: input.profile }),\n },\n groups,\n totalTokens: groups.reduce((total, group) => total + group.tokens, 0),\n inactiveTokens: groups.reduce((total, group) => total + group.inactiveTokens, 0),\n ...(input.systemPrompt === undefined ? {} : { systemPrompt: input.systemPrompt }),\n estimator: CONTEXT_PROJECTION_ESTIMATOR,\n };\n}\n"],"mappings":";;;;;;;;;;;;;AAwBA,MAAa,6BAA6B;;AAG1C,MAAa,wBAAwB;;AAGrC,MAAa,uBAAuB;AACpC,MAAa,kCAAkC;;AAG/C,MAAa,+BAA+B;;AAM5C,MAAM,aAAa;;AAEnB,MAAM,aAAa;AAGnB,MAAM,aAA+C;CAAE,OAAO;CAAG,OAAO;CAAG,QAAQ;CAAG,QAAQ;CAAG,MAAM;AAAE;AACzG,MAAM,eAAkD;CAAE,WAAW;CAAG,KAAK;CAAG,QAAQ;CAAG,MAAM;AAAE;AA6GnG,SAAS,YAAY,OAA0D;CAC7E,IAAI,UAAU,WAAW,OAAO;CAChC,IAAI,UAAU,cAAc,OAAO;CACnC,OAAO;AACT;AAEA,SAAS,MAAM,MAA6B,OAAsC;CAChF,OAAO,aAAa,KAAK,UAAU,aAAa,MAAM,WAAW,KAAK,KAAK,cAAc,MAAM,IAAI;AACrG;AAEA,SAAgB,eAAe,OAAkD;CAC/E,MAAM,0BAAU,IAAI,IAAoB;CAKxC,MAAM,WAAW,IAAY,OAAe,SAAiC;EAC3E,IAAI,CAAC,QAAQ,IAAI,EAAE,GAAG,QAAQ,IAAI,IAAI;GAAE;GAAM;GAAO,OAAO,CAAC;EAAE,CAAC;CAClE;CACA,IAAI,MAAM,WAAW,QAAQ,MAAM,WAAW,MAAM,WAAW,OAAO;CACtE,KAAK,MAAM,SAAS,MAAM,QAAQ,QAAQ,MAAM,IAAI,MAAM,OAAO,MAAM,IAAI;CAI3E,KAAK,MAAM,UAAU,MAAM,SAAS,QAAQ,QAAQ,QAAQ,QAAQ;CAEpE,MAAM,SAAS,OAAe,MAA6B,gBAAsD;EAC/G,MAAM,aAAa,eAAe,MAAM,YAAY;EACpD,MAAM,KAAK,YAAY,QAAQ;EAC/B,MAAM,OAAO,YAAY,QAAQ;EACjC,QAAQ,IAAI,cAAc,WAAW,aAAc,WAAW,SAAS,KAAM,IAAI,IAAI;EACrF,QAAQ,IAAI,EAAE,CAAC,EAAE,MAAM,KAAK,IAAI;CAClC;CAEA,KAAK,MAAM,UAAU,MAAM,SAAS;EAIlC,MAAM,QACJ,OAAO,SAAS,cACX,OAAO,eAAe,OAAO,MAC9B,OAAO,SAAS,QACd,OAAO,IAAI,MAAM,CAAqB,IACtC;EACR,KAAK,MAAM,QAAQ,OAAO,OAAO;GAC/B,MAAM,OAAO,cAAc,MAAM,MAAM,WAAW;GAClD,MAAM,OAA8B;IAClC,MAAM,KAAK;IACX,UAAU;IACV,QAAQ,OAAO,SAAS,SAAS,SAAS,OAAO;IACjD;IACA,QAAQ,KAAK;IACb,QAAQ,KAAK;GACf;GACA,IAAI,OAAO,SAAS,QAAQ;IAC1B,QAAQ,YAAY,YAAY,MAAM;IACtC,QAAQ,IAAI,UAAU,CAAC,EAAE,MAAM,KAAK,IAAI;GAC1C,OACE,MAAM,OAAO,MAAM,KAAK,kBAAkB;EAE9C;CACF;CAEA,KAAK,MAAM,SAAS,MAAM,QACxB,MACE,MAAM,OACN;EACE,MAAM,MAAM;EACZ,UAAU;EACV,QAAQ,YAAY,MAAM,KAAK;EAC/B,OAAO,MAAM;EACb,QAAQ,MAAM,gBAAgB;EAG9B,QAAQ;CACV,GACA,MAAM,kBACR;CAGF,MAAM,SAAmC,CAAC,GAAG,QAAQ,QAAQ,CAAC,CAAC,CAC5D,KAAK,CAAC,IAAI,YAAY;EACrB,MAAM,QAAQ,CAAC,GAAG,OAAO,KAAK,CAAC,CAAC,KAAK,KAAK;EAI1C,OAAO;GACL;GACA,OAAO,OAAO;GACd,MAAM,OAAO;GACb;GACA,QAAQ,MAAM,QAAQ,OAAO,SAAS,SAAS,KAAK,SAAS,KAAK,SAAS,IAAI,CAAC;GAChF,gBAAgB,MAAM,QAAQ,OAAO,SAAS,SAAS,KAAK,SAAS,IAAI,KAAK,SAAS,CAAC;EAC1F;CACF,CAAC,CAAC,CACD,MAAM,MAAM,UAAU,WAAW,KAAK,QAAQ,WAAW,MAAM,SAAS,KAAK,MAAM,cAAc,MAAM,KAAK,CAAC;CAEhH,OAAO;EACL,SAAA;EACA,UAAU,MAAM;EAChB,WAAW;GACT,WAAW,MAAM;GACjB,SAAS,CAAC,GAAG,MAAM,OAAO;GAC1B,GAAI,MAAM,YAAY,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS,MAAM,QAAQ;EAClE;EACA;EACA,aAAa,OAAO,QAAQ,OAAO,UAAU,QAAQ,MAAM,QAAQ,CAAC;EACpE,gBAAgB,OAAO,QAAQ,OAAO,UAAU,QAAQ,MAAM,gBAAgB,CAAC;EAC/E,GAAI,MAAM,iBAAiB,KAAA,IAAY,CAAC,IAAI,EAAE,cAAc,MAAM,aAAa;EAC/E,WAAW;CACb;AACF"}
1
+ {"version":3,"file":"contextProjection.mjs","names":[],"sources":["../src/services/contextProjection/index.ts"],"sourcesContent":["/** What admitted a package into the composition. */\nexport interface PackageAttribution {\n /** `major` for a layer package, `domain` for a plugin a domain carries. */\n kind: 'major' | 'domain';\n /** The major mode name, or the domain name. */\n mode: string;\n /** The layer the package was listed under; absent for a domain plugin. */\n layer?: string;\n}\n\nimport type { ContextPromptStage } from '../../types/contextApi';\nimport { type CountTokens, type ToolEntry, type ToolSource, tokensForTool } from '../toolInventory';\n\n/**\n * What the session is carrying, and what carrying it costs.\n *\n * Grouped by the mode that admitted each package rather than by where the tool\n * came from, because a mode is the thing a reader can switch off. The mechanism\n * (extension, MCP server, plugin) orders rows inside a group instead, so the\n * expensive thing is still easy to find.\n *\n * Schemas never travel. Only names, kinds, owners and integer counts do, which\n * keeps the payload small enough to republish whenever the composition changes.\n */\nexport const CONTEXT_PROJECTION_VERSION = 1;\n\n/** Journal entry the cockpit reads; mirrored by CONTEXT_ENTRY_TYPE in po-web. */\nexport const PO_CONTEXT_ENTRY_TYPE = 'po-context';\n\n/** Session selection and transition journal identities, not transport channels. */\nexport const PO_CONFIG_ENTRY_TYPE = 'po:config:v1';\nexport const PO_CONFIG_TRANSITION_ENTRY_TYPE = 'po:transition:v1';\n\n/** The tokenizer these counts came from, so a reader can judge them. */\nexport const CONTEXT_PROJECTION_ESTIMATOR = 'gpt-tokenizer';\n\nexport type ContextGroupKind = 'major' | 'minor' | 'domain' | 'shared' | 'core';\nexport type ContextItemSource = 'extension' | 'mcp' | 'plugin' | 'core';\n\n/** Everything the runtime could not attribute lands here rather than vanishing. */\nconst CORE_GROUP = 'core';\n/** Pi's own tools have no package; the runtime itself is the owner. */\nconst CORE_OWNER = 'pi';\nconst MCP_KEY_PREFIX = 'mcp:';\n\nconst KIND_ORDER: Record<ContextGroupKind, number> = { major: 0, minor: 1, domain: 2, shared: 3, core: 4 };\nconst SOURCE_ORDER: Record<ContextItemSource, number> = { extension: 0, mcp: 1, plugin: 2, core: 3 };\n\nexport interface ContextItemProjection {\n readonly name: string;\n readonly itemKind: 'tool' | 'skill';\n readonly source: ContextItemSource;\n /** The package, server, or plugin that registered it. */\n readonly owner: string;\n readonly tokens: number;\n readonly active: boolean;\n}\n\nexport interface ContextGroupProjection {\n readonly id: string;\n readonly label: string;\n readonly kind: ContextGroupKind;\n readonly items: readonly ContextItemProjection[];\n /** What the group costs now: active items only. */\n readonly tokens: number;\n /** What its gated items would add if switched on. */\n readonly inactiveTokens: number;\n}\n\n/**\n * What the assembled system prompt costs, and how complete the figure is.\n *\n * Kept out of `groups` on purpose. Every tool's prose and every skill's listing\n * already count inside the group subtotals, and those are slices of this same\n * text, so folding it into `totalTokens` would bill them twice.\n */\nexport interface ContextSystemPromptProjection {\n readonly tokens: number;\n readonly stage: ContextPromptStage;\n}\n\nexport interface ContextProjection {\n readonly version: typeof CONTEXT_PROJECTION_VERSION;\n readonly revision: number;\n /** Selection snapshot used to build this projection, replayable after reconnect. */\n readonly selection: {\n readonly majorMode: string;\n readonly domains: readonly string[];\n readonly profile?: string;\n /** Requested profile when none is active; null records an explicit None pick. Absent on legacy records. */\n readonly requestedProfile?: string | null;\n };\n readonly groups: readonly ContextGroupProjection[];\n readonly totalTokens: number;\n readonly inactiveTokens: number;\n readonly systemPrompt?: ContextSystemPromptProjection;\n readonly estimator: typeof CONTEXT_PROJECTION_ESTIMATOR;\n}\n\nexport interface ContextConditionalAttribution {\n readonly kind: 'minor' | 'domain' | 'shared';\n readonly mode: string;\n readonly label?: string;\n}\n\nexport interface ContextToolInventory extends ToolEntry {\n /** A contribution-level gate is more specific than its package's major-mode owner. */\n readonly contextAttribution?: ContextConditionalAttribution;\n}\n\nexport interface ContextToolSource extends Omit<ToolSource, 'tools'> {\n readonly tools: readonly ContextToolInventory[];\n}\n\n/** Skill fields needed to project cost and serve detail. Virtual resources may not have a filesystem path. */\nexport interface ContextSkillInventory {\n readonly name: string;\n readonly description: string;\n readonly group: 'extensions' | 'help' | 'plugins' | 'default';\n readonly owner: string;\n readonly modelInvocable: boolean;\n readonly promptTokens?: number;\n readonly filePath?: string;\n /** The skill's instructions, when the host holds them; read only when a reader opens the row. */\n readonly body?: string;\n readonly contextAttribution?: ContextConditionalAttribution;\n}\n\nexport interface ContextProjectionInput {\n readonly revision: number;\n readonly majorMode: string;\n readonly profile?: string;\n readonly requestedProfile?: string | null;\n /** Additional groups supplied by the composition owner, in display order. */\n readonly groups: readonly { readonly id: string; readonly label: string; readonly kind: ContextGroupKind }[];\n /** Domains the composition resolved under, whether or not they carried a plugin. */\n readonly domains: readonly string[];\n readonly sources: readonly ContextToolSource[];\n readonly skills: readonly ContextSkillInventory[];\n /**\n * Owner identifier to the mode that admitted it.\n *\n * Keys are package names for extensions and layer packages, and plugin or\n * server names for anything a domain carries. The caller owns this join\n * because only it knows how a given deployment resolved its plugins.\n */\n readonly attribution: Readonly<Record<string, PackageAttribution>>;\n readonly countTokens: CountTokens;\n /** Absent until the session has a prompt to report. */\n readonly systemPrompt?: ContextSystemPromptProjection;\n}\n\n/** Reads only replayable selection fields, accepting legacy projections without a request marker. */\nexport function readProjectedSelection(data: unknown): ContextProjection['selection'] | undefined {\n if (typeof data !== 'object' || data === null || Array.isArray(data) || !('selection' in data)) return undefined;\n const selection = data.selection;\n if (typeof selection !== 'object' || selection === null || Array.isArray(selection)) return undefined;\n if (!('majorMode' in selection) || typeof selection.majorMode !== 'string') return undefined;\n if (\n !('domains' in selection) ||\n !Array.isArray(selection.domains) ||\n !selection.domains.every((domain) => typeof domain === 'string')\n )\n return undefined;\n const profile = 'profile' in selection ? selection.profile : undefined;\n if (profile !== undefined && typeof profile !== 'string') return undefined;\n const requestedProfile = 'requestedProfile' in selection ? selection.requestedProfile : undefined;\n return {\n majorMode: selection.majorMode,\n domains: [...selection.domains],\n profile,\n ...(typeof requestedProfile === 'string' || requestedProfile === null ? { requestedProfile } : {}),\n };\n}\n\ninterface Bucket {\n kind: ContextGroupKind;\n label: string;\n items: ContextItemProjection[];\n}\n\nfunction skillSource(group: ContextSkillInventory['group']): ContextItemSource {\n if (group === 'plugins') return 'plugin';\n if (group === 'extensions') return 'extension';\n return 'core';\n}\n\nfunction byRow(left: ContextItemProjection, right: ContextItemProjection): number {\n return SOURCE_ORDER[left.source] - SOURCE_ORDER[right.source] || left.name.localeCompare(right.name);\n}\n\nexport function projectContext(input: ContextProjectionInput): ContextProjection {\n const buckets = new Map<string, Bucket>();\n\n // Declared before anything is placed, so a mode with nothing attributed to it\n // still appears. An empty group is a fact worth showing: it says the mode is\n // on and cheap, not that the runtime forgot about it.\n const declare = (id: string, label: string, kind: ContextGroupKind): void => {\n if (!buckets.has(id)) buckets.set(id, { kind, label, items: [] });\n };\n if (input.majorMode) declare(input.majorMode, input.majorMode, 'major');\n for (const group of input.groups) declare(group.id, group.label, group.kind);\n // A domain that contributed no plugin is still part of what the session is\n // running under, and a reader comparing the panel against the footer would\n // otherwise read its absence as the panel being out of date.\n for (const domain of input.domains) declare(domain, domain, 'domain');\n\n const place = (owner: string, item: ContextItemProjection, conditional?: ContextConditionalAttribution): void => {\n const attributed = conditional ?? input.attribution[owner];\n const id = attributed?.mode ?? CORE_GROUP;\n const kind = attributed?.kind ?? 'core';\n declare(id, attributed && 'label' in attributed ? (attributed.label ?? id) : id, kind);\n buckets.get(id)?.items.push(item);\n };\n\n for (const source of input.sources) {\n // An extension is owned by its package, an MCP tool by the server that\n // advertised it. `label` is display text like `scaffold · mcp`, so the\n // identifier comes from the key instead: an owner is joined on, not read.\n const owner =\n source.kind === 'extension'\n ? (source.packageName ?? source.key)\n : source.kind === 'mcp'\n ? source.key.slice(MCP_KEY_PREFIX.length)\n : CORE_OWNER;\n for (const tool of source.tools) {\n const cost = tokensForTool(tool, input.countTokens);\n const item: ContextItemProjection = {\n name: tool.name,\n itemKind: 'tool',\n source: source.kind === 'core' ? 'core' : source.kind,\n owner,\n tokens: cost.totalTokens,\n active: tool.active,\n };\n if (source.kind === 'core') {\n declare(CORE_GROUP, CORE_GROUP, 'core');\n buckets.get(CORE_GROUP)?.items.push(item);\n } else {\n place(owner, item, tool.contextAttribution);\n }\n }\n }\n\n for (const skill of input.skills) {\n place(\n skill.owner,\n {\n name: skill.name,\n itemKind: 'skill',\n source: skillSource(skill.group),\n owner: skill.owner,\n tokens: skill.promptTokens ?? 0,\n // A skill is always offered to the model; there is no inactive state for\n // it the way there is for a tool Pi has filtered out.\n active: true,\n },\n skill.contextAttribution,\n );\n }\n\n const groups: ContextGroupProjection[] = [...buckets.entries()]\n .map(([id, bucket]) => {\n const items = [...bucket.items].sort(byRow);\n // Only what is actually sent counts. Pi builds the prompt from its\n // active tools, so a gated one costs nothing until it is switched on,\n // and folding it into the total would overstate the bill by a third.\n return {\n id,\n label: bucket.label,\n kind: bucket.kind,\n items,\n tokens: items.reduce((total, item) => total + (item.active ? item.tokens : 0), 0),\n inactiveTokens: items.reduce((total, item) => total + (item.active ? 0 : item.tokens), 0),\n };\n })\n .sort((left, right) => KIND_ORDER[left.kind] - KIND_ORDER[right.kind] || left.label.localeCompare(right.label));\n\n return {\n version: CONTEXT_PROJECTION_VERSION,\n revision: input.revision,\n selection: {\n majorMode: input.majorMode,\n domains: [...input.domains],\n ...(input.profile === undefined ? {} : { profile: input.profile }),\n ...(input.requestedProfile === undefined ? {} : { requestedProfile: input.requestedProfile }),\n },\n groups,\n totalTokens: groups.reduce((total, group) => total + group.tokens, 0),\n inactiveTokens: groups.reduce((total, group) => total + group.inactiveTokens, 0),\n ...(input.systemPrompt === undefined ? {} : { systemPrompt: input.systemPrompt }),\n estimator: CONTEXT_PROJECTION_ESTIMATOR,\n };\n}\n"],"mappings":"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"}
@@ -75,6 +75,8 @@ export interface HeadlessSessionHostOptions {
75
75
  environment: Readonly<Record<string, string | undefined>>;
76
76
  selection: PoHeadlessSelection;
77
77
  selectionOverrides?: readonly ('majorMode' | 'domains' | 'profile')[];
78
+ /** Profile intent admitted by the launcher, retained when the requested profile is unavailable. */
79
+ requestedProfile?: string | null;
78
80
  /** The only tools the agent may see or call, whatever the selection and packages contribute. */
79
81
  allowedTools?: readonly string[];
80
82
  inheritedSelection?: () => Partial<PoHeadlessSelection> | Promise<Partial<PoHeadlessSelection>>;
@@ -1 +1 @@
1
- {"version":3,"file":"headlessSessionHost.d.mts","names":[],"sources":["../src/systems/main/types/headlessSessionHost.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;YAwBY,iBAAiB,SAC3B,KACE;;YAYQ,4BAA4B,KACtC;;iBAKe;;WAEN;WACA,cAAc;;WAEd;aAAyC;aAAuB;;;EAEzE,aAAa,QAAQ,OAAO;IAAU,OAAO,MAAM;IAAM,gBAAgB;;;WAEhE,gBAAgB,OAAO,8BAA8B;;iBAE/C;EACf;;EAEA,UAAU;EACV;EACA;;EAEA;EACA;;EAEA;EACA,iBAAiB;EACjB,oBAAoB;EACpB;;EAEA;;EAEA;EACA;EACA;;EAEA;;WAES,kBAAkB;EAC3B;EACA,aAAa,SAAS;EACtB,WAAW;EACX;;EAEA;EACA,2BAA2B,QAAQ,uBAAuB,QAAQ,QAAQ;EAC1E,qBAAqB;;EAErB,+BAA+B;;EAE/B,mCAAmC;;EAEnC,sBAAsB;EACtB,mBAAmB;EACnB,0BACE,YAAY,gBAAgB,mCAC5B,WAAW;EAEb,iBAAiB,SAAS,SAAS,WAAW,wBAAwB;EACtE,YAAY;;EAEZ;EACA,YAAY,KAAK,kCAAkC,QAAQ,KAAK;;EAEhE;;EAEA,qBAAqB,SAAS,+BAA+B,QAAQ;;iBAKtD;WACN,SAAS;;;;;WAKT,eAAe;EACxB,uBAAuB,QAAQ;;EAE/B,WAAW,qBAAqB;;WAEvB,iBAAiB;WACjB,MAAM;WACN,aAAa;WACb,YAAY;;WAEZ,kBAAkB,MAAM;;WAExB,mBAAmB,yCAAyC;;EAErE,gBAAgB,qBAAqB,uBAAuB,uCAAuC;;EAEnG,kBAAkB,eAAe,0BAA0B;WAClD;;EAET,aAAa,gBAAgB;EAC7B,oBAAoB,WAAW,OAAO;;EAEtC,aAAa,WAAW,WAAW;;EAEnC,kBAAkB,WAAW;EAC7B,qBAAqB,OAAO;;EAE5B,mBAAmB;EACnB,QAAQ,UAAU,8BAA8B"}
1
+ {"version":3,"file":"headlessSessionHost.d.mts","names":[],"sources":["../src/systems/main/types/headlessSessionHost.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;YAwBY,iBAAiB,SAC3B,KACE;;YAYQ,4BAA4B,KACtC;;iBAKe;;WAEN;WACA,cAAc;;WAEd;aAAyC;aAAuB;;;EAEzE,aAAa,QAAQ,OAAO;IAAU,OAAO,MAAM;IAAM,gBAAgB;;;WAEhE,gBAAgB,OAAO,8BAA8B;;iBAE/C;EACf;;EAEA,UAAU;EACV;EACA;;EAEA;EACA;;EAEA;EACA,iBAAiB;EACjB,oBAAoB;EACpB;;EAEA;;EAEA;EACA;EACA;;EAEA;;WAES,kBAAkB;EAC3B;EACA,aAAa,SAAS;EACtB,WAAW;EACX;;EAEA;;EAEA;EACA,2BAA2B,QAAQ,uBAAuB,QAAQ,QAAQ;EAC1E,qBAAqB;;EAErB,+BAA+B;;EAE/B,mCAAmC;;EAEnC,sBAAsB;EACtB,mBAAmB;EACnB,0BACE,YAAY,gBAAgB,mCAC5B,WAAW;EAEb,iBAAiB,SAAS,SAAS,WAAW,wBAAwB;EACtE,YAAY;;EAEZ;EACA,YAAY,KAAK,kCAAkC,QAAQ,KAAK;;EAEhE;;EAEA,qBAAqB,SAAS,+BAA+B,QAAQ;;iBAKtD;WACN,SAAS;;;;;WAKT,eAAe;EACxB,uBAAuB,QAAQ;;EAE/B,WAAW,qBAAqB;;WAEvB,iBAAiB;WACjB,MAAM;WACN,aAAa;WACb,YAAY;;WAEZ,kBAAkB,MAAM;;WAExB,mBAAmB,yCAAyC;;EAErE,gBAAgB,qBAAqB,uBAAuB,uCAAuC;;EAEnG,kBAAkB,eAAe,0BAA0B;WAClD;;EAET,aAAa,gBAAgB;EAC7B,oBAAoB,WAAW,OAAO;;EAEtC,aAAa,WAAW,WAAW;;EAEnC,kBAAkB,WAAW;EAC7B,qBAAqB,OAAO;;EAE5B,mBAAmB;EACnB,QAAQ,UAAU,8BAA8B"}
package/dist/history.cjs CHANGED
@@ -19,6 +19,7 @@ exports.identifyWorkspace = require_sync$1.identifyWorkspace;
19
19
  exports.listSavedSessionRecords = require_index.listSavedSessionRecords;
20
20
  exports.listSavedSessions = require_index.listSavedSessions;
21
21
  exports.localWorkspaceRecords = require_workspaceRecords.localWorkspaceRecords;
22
+ exports.readSavedSelection = require_index.readSavedSelection;
22
23
  exports.readSqliteTranscript = require_index$1.readSqliteTranscript;
23
24
  exports.readWorkspaceMarker = require_sync$1.readWorkspaceMarker;
24
25
  exports.validateDurableSessionFile = require_sqliteSessionStorage.validateDurableSessionFile;
@@ -1,6 +1,6 @@
1
1
  import { HistoryOwnership, HistoryOwnershipLease } from "./src/services/historyOwnership/type.mjs";
2
2
  import { HistoryOwnershipOptions, createHistoryOwnership, createV4HistoryOwnership, historyOwnershipLockPath } from "./historyOwnership.mjs";
3
- import { RemovedSession, SavedSessionExecution, SavedSessionRecord, listSavedSessionRecords, listSavedSessions } from "./src/services/sqliteSessionHistory/index.mjs";
3
+ import { RemovedSession, SavedSessionExecution, SavedSessionRecord, listSavedSessionRecords, listSavedSessions, readSavedSelection } from "./src/services/sqliteSessionHistory/index.mjs";
4
4
  import { SqliteTranscriptOwnership, readSqliteTranscript } from "./src/services/sqliteTranscriptReader/index.mjs";
5
5
  import { validateDurableSessionFile } from "./sqliteSessionStorage.mjs";
6
6
  import { OpenSessionRecord, OpenSessionRegistry, OpenSessionRegistryOptions } from "./src/services/openSessionRegistry/openSessionRecords.mjs";
@@ -10,4 +10,4 @@ import { WorkspaceIdentity, WorkspaceRecord, WorkspaceRegistry, WorkspaceRegistr
10
10
  import { createWorkspaceRegistry, identifyWorkspace, readWorkspaceMarker, writeWorkspaceMarker } from "./src/services/workspaceRegistry/sync.mjs";
11
11
  import "./src/services/workspaceRegistry/index.mjs";
12
12
  import { createRequestReceipts } from "./src/services/durableRequestReceipts/index.mjs";
13
- export { type HistoryOwnership, type HistoryOwnershipLease, type HistoryOwnershipOptions, type OpenSessionRecord, type OpenSessionRegistry, type OpenSessionRegistryOptions, type RemovedSession, type SavedSessionExecution, type SavedSessionRecord, type SqliteTranscriptOwnership, type WorkspaceIdentity, type WorkspaceRecord, type WorkspaceRegistry, type WorkspaceRegistryOptions, createHistoryOwnership, createOpenSessionRegistry, createRequestReceipts, createV4HistoryOwnership, createWorkspaceRegistry, historyOwnershipLockPath, identifyWorkspace, listSavedSessionRecords, listSavedSessions, localWorkspaceRecords, readSqliteTranscript, readWorkspaceMarker, validateDurableSessionFile, writeWorkspaceMarker };
13
+ export { type HistoryOwnership, type HistoryOwnershipLease, type HistoryOwnershipOptions, type OpenSessionRecord, type OpenSessionRegistry, type OpenSessionRegistryOptions, type RemovedSession, type SavedSessionExecution, type SavedSessionRecord, type SqliteTranscriptOwnership, type WorkspaceIdentity, type WorkspaceRecord, type WorkspaceRegistry, type WorkspaceRegistryOptions, createHistoryOwnership, createOpenSessionRegistry, createRequestReceipts, createV4HistoryOwnership, createWorkspaceRegistry, historyOwnershipLockPath, identifyWorkspace, listSavedSessionRecords, listSavedSessions, localWorkspaceRecords, readSavedSelection, readSqliteTranscript, readWorkspaceMarker, validateDurableSessionFile, writeWorkspaceMarker };
package/dist/history.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createHistoryOwnership, createV4HistoryOwnership, historyOwnershipLockPath } from "./historyOwnership.mjs";
2
2
  import { validateDurableSessionFile } from "./sqliteSessionStorage.mjs";
3
- import { listSavedSessionRecords, listSavedSessions } from "./src/services/sqliteSessionHistory/index.mjs";
3
+ import { listSavedSessionRecords, listSavedSessions, readSavedSelection } from "./src/services/sqliteSessionHistory/index.mjs";
4
4
  import { readSqliteTranscript } from "./src/services/sqliteTranscriptReader/index.mjs";
5
5
  import { createOpenSessionRegistry } from "./src/services/openSessionRegistry/sync.mjs";
6
6
  import "./src/services/openSessionRegistry/index.mjs";
@@ -8,4 +8,4 @@ import { localWorkspaceRecords } from "./src/services/workspaceRegistry/workspac
8
8
  import { createWorkspaceRegistry, identifyWorkspace, readWorkspaceMarker, writeWorkspaceMarker } from "./src/services/workspaceRegistry/sync.mjs";
9
9
  import "./src/services/workspaceRegistry/index.mjs";
10
10
  import { createRequestReceipts } from "./src/services/durableRequestReceipts/index.mjs";
11
- export { createHistoryOwnership, createOpenSessionRegistry, createRequestReceipts, createV4HistoryOwnership, createWorkspaceRegistry, historyOwnershipLockPath, identifyWorkspace, listSavedSessionRecords, listSavedSessions, localWorkspaceRecords, readSqliteTranscript, readWorkspaceMarker, validateDurableSessionFile, writeWorkspaceMarker };
11
+ export { createHistoryOwnership, createOpenSessionRegistry, createRequestReceipts, createV4HistoryOwnership, createWorkspaceRegistry, historyOwnershipLockPath, identifyWorkspace, listSavedSessionRecords, listSavedSessions, localWorkspaceRecords, readSavedSelection, readSqliteTranscript, readWorkspaceMarker, validateDurableSessionFile, writeWorkspaceMarker };
@@ -1,6 +1,9 @@
1
1
  const require_runtime = require("../../../_virtual/_rolldown/runtime.cjs");
2
- const require_docs = require("../durableSession/docs.cjs");
2
+ const require_contextProjection = require("../../../contextProjection.cjs");
3
+ const require_docs = require("../durableNavigation/docs.cjs");
4
+ const require_docs$1 = require("../durableSession/docs.cjs");
3
5
  const require_sqliteSessionStorage = require("../../../sqliteSessionStorage.cjs");
6
+ const require_index = require("../transcriptPages/index.cjs");
4
7
  let node_path = require("node:path");
5
8
  node_path = require_runtime.__toESM(node_path, 1);
6
9
  let _earendil_works_pi_durable = require("@earendil-works/pi-durable");
@@ -49,6 +52,28 @@ async function firstUserEntry(storage, conversationId, before) {
49
52
  } while (cursor);
50
53
  }
51
54
  /**
55
+ * The selection a stored session last projected into its active conversation, read-only. The newest valid
56
+ * projection wins even when it names no profile: that absence is the saved answer, not a gap to fill from
57
+ * an older projection. A fork reads its own history, not the abandoned tail of its source.
58
+ */
59
+ async function readSavedSelection(sessions, sessionId) {
60
+ const storage = await sessions.openReadOnly(sessionId, require_sqliteSessionStorage.DURABLE_BACKGROUND_CONTEXT);
61
+ const session = (0, _earendil_works_pi_durable.createSession)(storage);
62
+ try {
63
+ const identity = await session.snapshot(require_docs$1.SessionIdentityDoc, require_sqliteSessionStorage.DURABLE_BACKGROUND_CONTEXT);
64
+ if (!identity || identity.id !== sessionId) throw new Error("Durable session identity mismatch");
65
+ const conversationId = (await session.snapshot(require_docs.DurableNavigationDoc, require_sqliteSessionStorage.DURABLE_BACKGROUND_CONTEXT))?.activeConversationId;
66
+ if (conversationId === null || conversationId === void 0) return void 0;
67
+ const projected = await require_index.findNewestDurableEntry((query, limit, cursor, scanContext) => storage.scanEntries({
68
+ conversationId,
69
+ ...query
70
+ }, limit, cursor, scanContext), require_sqliteSessionStorage.DURABLE_BACKGROUND_CONTEXT, (entry) => entry.type === "custom" && entry.customType === "po-context" && require_contextProjection.readProjectedSelection(entry.data) !== void 0);
71
+ return projected?.type === "custom" ? require_contextProjection.readProjectedSelection(projected.data) : void 0;
72
+ } finally {
73
+ await session.close(require_sqliteSessionStorage.DURABLE_BACKGROUND_CONTEXT);
74
+ }
75
+ }
76
+ /**
52
77
  * Lists the stored sessions the injected storage holds, read-only. One file that cannot be read is
53
78
  * skipped and handed to `onUnreadable`, so it never empties the rest of the history.
54
79
  */
@@ -65,9 +90,9 @@ async function listSavedSessionRecords(sessions, workspaceRoot, activeSessionIds
65
90
  }
66
91
  const session = (0, _earendil_works_pi_durable.createSession)(storage);
67
92
  try {
68
- const identity = await session.snapshot(require_docs.SessionIdentityDoc, require_sqliteSessionStorage.DURABLE_BACKGROUND_CONTEXT);
93
+ const identity = await session.snapshot(require_docs$1.SessionIdentityDoc, require_sqliteSessionStorage.DURABLE_BACKGROUND_CONTEXT);
69
94
  if (!identity || identity.id !== id) throw new Error("Durable session identity mismatch");
70
- const metadata = await session.snapshot(require_docs.SessionMetadataDoc, require_sqliteSessionStorage.DURABLE_BACKGROUND_CONTEXT);
95
+ const metadata = await session.snapshot(require_docs$1.SessionMetadataDoc, require_sqliteSessionStorage.DURABLE_BACKGROUND_CONTEXT);
71
96
  if (!metadata?.workspaceRoot) continue;
72
97
  const execution = readSavedExecution(metadata.execution, metadata.workspaceRoot);
73
98
  if (!execution || execution.groupingRoot !== workspaceRoot || workspaceId && execution.workspaceId !== workspaceId) continue;
@@ -109,5 +134,6 @@ async function listSavedSessions(sessions, workspaceRoot, activeSessionIds, work
109
134
  exports.listSavedSessionRecords = listSavedSessionRecords;
110
135
  exports.listSavedSessions = listSavedSessions;
111
136
  exports.readSavedExecution = readSavedExecution;
137
+ exports.readSavedSelection = readSavedSelection;
112
138
 
113
139
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["path","context","createSession","SessionIdentityDoc","SessionMetadataDoc"],"sources":["../../../../src/services/sqliteSessionHistory/index.ts"],"sourcesContent":["import path from 'node:path';\n\nimport {\n createSession,\n type ConversationId,\n type Cursor,\n type EntryId,\n type EntryRecord,\n type Storage,\n} from '@earendil-works/pi-durable';\n\nimport type { PoSessionStorageService } from '../../schemas/platform';\nimport { DURABLE_BACKGROUND_CONTEXT as context, SessionIdentityDoc, SessionMetadataDoc } from '../sqliteSessionStorage';\n\nexport interface SavedSession {\n id: string;\n name?: string;\n firstMessage: string;\n createdAt: string;\n updatedAt: string;\n}\n\n/** A session hidden from History that can still be restored until `purgeAt` (ISO 8601). */\nexport interface RemovedSession extends SavedSession {\n removedAt: string;\n purgeAt: string;\n}\n\n/** Private execution context, never included in the HTTP history summary. */\nexport interface SavedSessionExecution {\n cwd: string;\n repoRoot: string;\n workspaceId: string;\n groupingRoot: string;\n parentSessionId?: string;\n sessionProvenance?: string;\n interaction?: 'interactive' | 'unattended';\n}\n\nexport interface SavedSessionRecord {\n summary: SavedSession;\n execution: SavedSessionExecution;\n}\n\nexport function readSavedExecution(raw: unknown, owner: string): SavedSessionExecution | undefined {\n if (typeof raw !== 'string') return undefined;\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return undefined;\n }\n if (typeof parsed !== 'object' || parsed === null) return undefined;\n const record = parsed as Record<string, unknown>;\n if (\n typeof record.cwd !== 'string' ||\n !path.isAbsolute(record.cwd) ||\n typeof record.repoRoot !== 'string' ||\n !path.isAbsolute(record.repoRoot) ||\n record.repoRoot !== owner ||\n typeof record.workspaceId !== 'string' ||\n record.workspaceId === '' ||\n typeof record.groupingRoot !== 'string' ||\n !path.isAbsolute(record.groupingRoot)\n )\n return undefined;\n if (record.parentSessionId !== undefined && typeof record.parentSessionId !== 'string') return undefined;\n if (record.sessionProvenance !== undefined && typeof record.sessionProvenance !== 'string') return undefined;\n if (record.interaction !== undefined && record.interaction !== 'interactive' && record.interaction !== 'unattended')\n return undefined;\n return {\n cwd: record.cwd,\n repoRoot: record.repoRoot,\n workspaceId: record.workspaceId,\n groupingRoot: record.groupingRoot,\n ...(typeof record.parentSessionId === 'string' ? { parentSessionId: record.parentSessionId } : {}),\n ...(typeof record.sessionProvenance === 'string' ? { sessionProvenance: record.sessionProvenance } : {}),\n ...(record.interaction === undefined ? {} : { interaction: record.interaction }),\n };\n}\n\n/** The oldest user message of one conversation's fork-aware history older than `before`, read only up to it. */\nasync function firstUserEntry(\n storage: Storage,\n conversationId: ConversationId,\n before: EntryId | undefined,\n): Promise<{ id: EntryId; message: NonNullable<EntryRecord['model']>[number] } | undefined> {\n let cursor: Cursor | undefined;\n do {\n const page = await storage.scanEntries(\n { conversationId, order: 'ascending', ...(before === undefined ? {} : { maxEntryId: (before - 1) as EntryId }) },\n 100,\n cursor,\n context,\n );\n for (const entry of page.items) {\n const message = entry.model?.find((item) => item.role === 'user');\n if (message) return { id: entry.id, message };\n }\n cursor = page.next;\n } while (cursor);\n return undefined;\n}\n/**\n * Lists the stored sessions the injected storage holds, read-only. One file that cannot be read is\n * skipped and handed to `onUnreadable`, so it never empties the rest of the history.\n */\nexport async function listSavedSessionRecords(\n sessions: PoSessionStorageService,\n workspaceRoot: string,\n activeSessionIds: ReadonlySet<string>,\n workspaceId?: string,\n onUnreadable?: (sessionId: string, error: unknown) => void,\n): Promise<SavedSessionRecord[]> {\n const result: SavedSessionRecord[] = [];\n for (const { sessionId: id, updatedAt } of await sessions.list(context)) {\n if (activeSessionIds.has(id)) continue;\n let storage: Storage;\n try {\n storage = await sessions.openReadOnly(id, context);\n } catch (error) {\n onUnreadable?.(id, error);\n continue;\n }\n const session = createSession(storage);\n try {\n const identity = await session.snapshot(SessionIdentityDoc, context);\n if (!identity || identity.id !== id) throw new Error('Durable session identity mismatch');\n const metadata = await session.snapshot(SessionMetadataDoc, context);\n if (!metadata?.workspaceRoot) continue;\n const execution = readSavedExecution(metadata.execution, metadata.workspaceRoot);\n if (\n !execution ||\n execution.groupingRoot !== workspaceRoot ||\n (workspaceId && execution.workspaceId !== workspaceId)\n )\n continue;\n let first: Awaited<ReturnType<typeof firstUserEntry>>;\n let conversationCursor: Cursor | undefined;\n do {\n const conversations = await storage.scanConversations({}, 100, conversationCursor, context);\n for (const conversation of conversations.items) {\n // A task owns a child's conversation; the session's own history is its ownerless ones.\n if (conversation.owner) continue;\n first = (await firstUserEntry(storage, conversation.id, first?.id)) ?? first;\n }\n conversationCursor = conversations.next;\n } while (conversationCursor);\n const content = first?.message.content;\n const firstMessage =\n typeof content === 'string'\n ? content\n : Array.isArray(content)\n ? content\n .filter((part) => part.type === 'text')\n .map((part) => part.text)\n .join('\\n')\n : '';\n result.push({\n summary: {\n id,\n ...(metadata.name ? { name: metadata.name } : {}),\n firstMessage,\n createdAt: new Date(identity.createdAt).toISOString(),\n updatedAt: new Date(updatedAt).toISOString(),\n },\n execution,\n });\n } catch (error) {\n onUnreadable?.(id, error);\n } finally {\n await session.close(context);\n }\n }\n return result.sort((a, b) => b.summary.updatedAt.localeCompare(a.summary.updatedAt));\n}\n\n/** Public summaries do not expose private execution metadata. */\nexport async function listSavedSessions(\n sessions: PoSessionStorageService,\n workspaceRoot: string,\n activeSessionIds: ReadonlySet<string>,\n workspaceId?: string,\n onUnreadable?: (sessionId: string, error: unknown) => void,\n): Promise<SavedSession[]> {\n return (await listSavedSessionRecords(sessions, workspaceRoot, activeSessionIds, workspaceId, onUnreadable)).map(\n (record) => record.summary,\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["path","context","createSession","SessionIdentityDoc","DurableNavigationDoc","findNewestDurableEntry","readProjectedSelection","SessionMetadataDoc"],"sources":["../../../../src/services/sqliteSessionHistory/index.ts"],"sourcesContent":["import path from 'node:path';\n\nimport {\n createSession,\n type ConversationId,\n type Cursor,\n type EntryId,\n type EntryRecord,\n type Storage,\n} from '@earendil-works/pi-durable';\n\nimport type { PoSessionStorageService } from '../../schemas/platform';\nimport { PO_CONTEXT_ENTRY_TYPE, readProjectedSelection, type ContextProjection } from '../contextProjection';\nimport { DurableNavigationDoc } from '../durableNavigation/docs';\nimport { DURABLE_BACKGROUND_CONTEXT as context, SessionIdentityDoc, SessionMetadataDoc } from '../sqliteSessionStorage';\nimport { findNewestDurableEntry } from '../transcriptPages';\n\nexport interface SavedSession {\n id: string;\n name?: string;\n firstMessage: string;\n createdAt: string;\n updatedAt: string;\n}\n\n/** A session hidden from History that can still be restored until `purgeAt` (ISO 8601). */\nexport interface RemovedSession extends SavedSession {\n removedAt: string;\n purgeAt: string;\n}\n\n/** Private execution context, never included in the HTTP history summary. */\nexport interface SavedSessionExecution {\n cwd: string;\n repoRoot: string;\n workspaceId: string;\n groupingRoot: string;\n parentSessionId?: string;\n sessionProvenance?: string;\n interaction?: 'interactive' | 'unattended';\n}\n\nexport interface SavedSessionRecord {\n summary: SavedSession;\n execution: SavedSessionExecution;\n}\n\nexport function readSavedExecution(raw: unknown, owner: string): SavedSessionExecution | undefined {\n if (typeof raw !== 'string') return undefined;\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return undefined;\n }\n if (typeof parsed !== 'object' || parsed === null) return undefined;\n const record = parsed as Record<string, unknown>;\n if (\n typeof record.cwd !== 'string' ||\n !path.isAbsolute(record.cwd) ||\n typeof record.repoRoot !== 'string' ||\n !path.isAbsolute(record.repoRoot) ||\n record.repoRoot !== owner ||\n typeof record.workspaceId !== 'string' ||\n record.workspaceId === '' ||\n typeof record.groupingRoot !== 'string' ||\n !path.isAbsolute(record.groupingRoot)\n )\n return undefined;\n if (record.parentSessionId !== undefined && typeof record.parentSessionId !== 'string') return undefined;\n if (record.sessionProvenance !== undefined && typeof record.sessionProvenance !== 'string') return undefined;\n if (record.interaction !== undefined && record.interaction !== 'interactive' && record.interaction !== 'unattended')\n return undefined;\n return {\n cwd: record.cwd,\n repoRoot: record.repoRoot,\n workspaceId: record.workspaceId,\n groupingRoot: record.groupingRoot,\n ...(typeof record.parentSessionId === 'string' ? { parentSessionId: record.parentSessionId } : {}),\n ...(typeof record.sessionProvenance === 'string' ? { sessionProvenance: record.sessionProvenance } : {}),\n ...(record.interaction === undefined ? {} : { interaction: record.interaction }),\n };\n}\n\n/** The oldest user message of one conversation's fork-aware history older than `before`, read only up to it. */\nasync function firstUserEntry(\n storage: Storage,\n conversationId: ConversationId,\n before: EntryId | undefined,\n): Promise<{ id: EntryId; message: NonNullable<EntryRecord['model']>[number] } | undefined> {\n let cursor: Cursor | undefined;\n do {\n const page = await storage.scanEntries(\n { conversationId, order: 'ascending', ...(before === undefined ? {} : { maxEntryId: (before - 1) as EntryId }) },\n 100,\n cursor,\n context,\n );\n for (const entry of page.items) {\n const message = entry.model?.find((item) => item.role === 'user');\n if (message) return { id: entry.id, message };\n }\n cursor = page.next;\n } while (cursor);\n return undefined;\n}\n/**\n * The selection a stored session last projected into its active conversation, read-only. The newest valid\n * projection wins even when it names no profile: that absence is the saved answer, not a gap to fill from\n * an older projection. A fork reads its own history, not the abandoned tail of its source.\n */\nexport async function readSavedSelection(\n sessions: PoSessionStorageService,\n sessionId: string,\n): Promise<ContextProjection['selection'] | undefined> {\n const storage = await sessions.openReadOnly(sessionId, context);\n const session = createSession(storage);\n try {\n const identity = await session.snapshot(SessionIdentityDoc, context);\n if (!identity || identity.id !== sessionId) throw new Error('Durable session identity mismatch');\n const conversationId = (await session.snapshot(DurableNavigationDoc, context))?.activeConversationId;\n if (conversationId === null || conversationId === undefined) return undefined;\n const projected = await findNewestDurableEntry(\n (query, limit, cursor, scanContext) =>\n storage.scanEntries({ conversationId, ...query }, limit, cursor, scanContext),\n context,\n (entry) =>\n entry.type === 'custom' &&\n entry.customType === PO_CONTEXT_ENTRY_TYPE &&\n readProjectedSelection(entry.data) !== undefined,\n );\n return projected?.type === 'custom' ? readProjectedSelection(projected.data) : undefined;\n } finally {\n await session.close(context);\n }\n}\n\n/**\n * Lists the stored sessions the injected storage holds, read-only. One file that cannot be read is\n * skipped and handed to `onUnreadable`, so it never empties the rest of the history.\n */\nexport async function listSavedSessionRecords(\n sessions: PoSessionStorageService,\n workspaceRoot: string,\n activeSessionIds: ReadonlySet<string>,\n workspaceId?: string,\n onUnreadable?: (sessionId: string, error: unknown) => void,\n): Promise<SavedSessionRecord[]> {\n const result: SavedSessionRecord[] = [];\n for (const { sessionId: id, updatedAt } of await sessions.list(context)) {\n if (activeSessionIds.has(id)) continue;\n let storage: Storage;\n try {\n storage = await sessions.openReadOnly(id, context);\n } catch (error) {\n onUnreadable?.(id, error);\n continue;\n }\n const session = createSession(storage);\n try {\n const identity = await session.snapshot(SessionIdentityDoc, context);\n if (!identity || identity.id !== id) throw new Error('Durable session identity mismatch');\n const metadata = await session.snapshot(SessionMetadataDoc, context);\n if (!metadata?.workspaceRoot) continue;\n const execution = readSavedExecution(metadata.execution, metadata.workspaceRoot);\n if (\n !execution ||\n execution.groupingRoot !== workspaceRoot ||\n (workspaceId && execution.workspaceId !== workspaceId)\n )\n continue;\n let first: Awaited<ReturnType<typeof firstUserEntry>>;\n let conversationCursor: Cursor | undefined;\n do {\n const conversations = await storage.scanConversations({}, 100, conversationCursor, context);\n for (const conversation of conversations.items) {\n // A task owns a child's conversation; the session's own history is its ownerless ones.\n if (conversation.owner) continue;\n first = (await firstUserEntry(storage, conversation.id, first?.id)) ?? first;\n }\n conversationCursor = conversations.next;\n } while (conversationCursor);\n const content = first?.message.content;\n const firstMessage =\n typeof content === 'string'\n ? content\n : Array.isArray(content)\n ? content\n .filter((part) => part.type === 'text')\n .map((part) => part.text)\n .join('\\n')\n : '';\n result.push({\n summary: {\n id,\n ...(metadata.name ? { name: metadata.name } : {}),\n firstMessage,\n createdAt: new Date(identity.createdAt).toISOString(),\n updatedAt: new Date(updatedAt).toISOString(),\n },\n execution,\n });\n } catch (error) {\n onUnreadable?.(id, error);\n } finally {\n await session.close(context);\n }\n }\n return result.sort((a, b) => b.summary.updatedAt.localeCompare(a.summary.updatedAt));\n}\n\n/** Public summaries do not expose private execution metadata. */\nexport async function listSavedSessions(\n sessions: PoSessionStorageService,\n workspaceRoot: string,\n activeSessionIds: ReadonlySet<string>,\n workspaceId?: string,\n onUnreadable?: (sessionId: string, error: unknown) => void,\n): Promise<SavedSession[]> {\n return (await listSavedSessionRecords(sessions, workspaceRoot, activeSessionIds, workspaceId, onUnreadable)).map(\n (record) => record.summary,\n );\n}\n"],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import { PoSessionStorageService } from "../../schemas/platform.mjs";
2
+ import { ContextProjection } from "../../../contextProjection.mjs";
2
3
  //#region src/services/sqliteSessionHistory/index.d.ts
3
4
  export interface SavedSession {
4
5
  id: string;
@@ -26,6 +27,12 @@ export interface SavedSessionRecord {
26
27
  summary: SavedSession;
27
28
  execution: SavedSessionExecution;
28
29
  }
30
+ /**
31
+ * The selection a stored session last projected into its active conversation, read-only. The newest valid
32
+ * projection wins even when it names no profile: that absence is the saved answer, not a gap to fill from
33
+ * an older projection. A fork reads its own history, not the abandoned tail of its source.
34
+ */
35
+ export declare function readSavedSelection(sessions: PoSessionStorageService, sessionId: string): Promise<ContextProjection['selection'] | undefined>;
29
36
  /**
30
37
  * Lists the stored sessions the injected storage holds, read-only. One file that cannot be read is
31
38
  * skipped and handed to `onUnreadable`, so it never empties the rest of the history.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../../../src/services/sqliteSessionHistory/index.ts"],"mappings":";;iBAciB;EACf;EACA;EACA;EACA;EACA;;;iBAIe,uBAAuB;EACtC;EACA;;;iBAIe;EACf;EACA;EACA;EACA;EACA;EACA;EACA;;iBAGe;EACf,SAAS;EACT,WAAW;;;;;;wBAkES,wBACpB,UAAU,yBACV,uBACA,kBAAkB,qBAClB,sBACA,gBAAgB,mBAAmB,0BAClC,QAAQ;;wBAiEW,kBACpB,UAAU,yBACV,uBACA,kBAAkB,qBAClB,sBACA,gBAAgB,mBAAmB,0BAClC,QAAQ"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../../../src/services/sqliteSessionHistory/index.ts"],"mappings":";;;iBAiBiB;EACf;EACA;EACA;EACA;EACA;;;iBAIe,uBAAuB;EACtC;EACA;;;iBAIe;EACf;EACA;EACA;EACA;EACA;EACA;EACA;;iBAGe;EACf,SAAS;EACT,WAAW;;;;;;;wBAmES,mBACpB,UAAU,yBACV,oBACC,QAAQ;;;;;wBA2BW,wBACpB,UAAU,yBACV,uBACA,kBAAkB,qBAClB,sBACA,gBAAgB,mBAAmB,0BAClC,QAAQ;;wBAiEW,kBACpB,UAAU,yBACV,uBACA,kBAAkB,qBAClB,sBACA,gBAAgB,mBAAmB,0BAClC,QAAQ"}
@@ -1,5 +1,8 @@
1
+ import { readProjectedSelection } from "../../../contextProjection.mjs";
2
+ import { DurableNavigationDoc } from "../durableNavigation/docs.mjs";
1
3
  import { SessionIdentityDoc, SessionMetadataDoc } from "../durableSession/docs.mjs";
2
4
  import { DURABLE_BACKGROUND_CONTEXT } from "../../../sqliteSessionStorage.mjs";
5
+ import { findNewestDurableEntry } from "../transcriptPages/index.mjs";
3
6
  import path from "node:path";
4
7
  import { createSession } from "@earendil-works/pi-durable";
5
8
  //#region src/services/sqliteSessionHistory/index.ts
@@ -47,6 +50,28 @@ async function firstUserEntry(storage, conversationId, before) {
47
50
  } while (cursor);
48
51
  }
49
52
  /**
53
+ * The selection a stored session last projected into its active conversation, read-only. The newest valid
54
+ * projection wins even when it names no profile: that absence is the saved answer, not a gap to fill from
55
+ * an older projection. A fork reads its own history, not the abandoned tail of its source.
56
+ */
57
+ async function readSavedSelection(sessions, sessionId) {
58
+ const storage = await sessions.openReadOnly(sessionId, DURABLE_BACKGROUND_CONTEXT);
59
+ const session = createSession(storage);
60
+ try {
61
+ const identity = await session.snapshot(SessionIdentityDoc, DURABLE_BACKGROUND_CONTEXT);
62
+ if (!identity || identity.id !== sessionId) throw new Error("Durable session identity mismatch");
63
+ const conversationId = (await session.snapshot(DurableNavigationDoc, DURABLE_BACKGROUND_CONTEXT))?.activeConversationId;
64
+ if (conversationId === null || conversationId === void 0) return void 0;
65
+ const projected = await findNewestDurableEntry((query, limit, cursor, scanContext) => storage.scanEntries({
66
+ conversationId,
67
+ ...query
68
+ }, limit, cursor, scanContext), DURABLE_BACKGROUND_CONTEXT, (entry) => entry.type === "custom" && entry.customType === "po-context" && readProjectedSelection(entry.data) !== void 0);
69
+ return projected?.type === "custom" ? readProjectedSelection(projected.data) : void 0;
70
+ } finally {
71
+ await session.close(DURABLE_BACKGROUND_CONTEXT);
72
+ }
73
+ }
74
+ /**
50
75
  * Lists the stored sessions the injected storage holds, read-only. One file that cannot be read is
51
76
  * skipped and handed to `onUnreadable`, so it never empties the rest of the history.
52
77
  */
@@ -104,6 +129,6 @@ async function listSavedSessions(sessions, workspaceRoot, activeSessionIds, work
104
129
  return (await listSavedSessionRecords(sessions, workspaceRoot, activeSessionIds, workspaceId, onUnreadable)).map((record) => record.summary);
105
130
  }
106
131
  //#endregion
107
- export { listSavedSessionRecords, listSavedSessions, readSavedExecution };
132
+ export { listSavedSessionRecords, listSavedSessions, readSavedExecution, readSavedSelection };
108
133
 
109
134
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["context"],"sources":["../../../../src/services/sqliteSessionHistory/index.ts"],"sourcesContent":["import path from 'node:path';\n\nimport {\n createSession,\n type ConversationId,\n type Cursor,\n type EntryId,\n type EntryRecord,\n type Storage,\n} from '@earendil-works/pi-durable';\n\nimport type { PoSessionStorageService } from '../../schemas/platform';\nimport { DURABLE_BACKGROUND_CONTEXT as context, SessionIdentityDoc, SessionMetadataDoc } from '../sqliteSessionStorage';\n\nexport interface SavedSession {\n id: string;\n name?: string;\n firstMessage: string;\n createdAt: string;\n updatedAt: string;\n}\n\n/** A session hidden from History that can still be restored until `purgeAt` (ISO 8601). */\nexport interface RemovedSession extends SavedSession {\n removedAt: string;\n purgeAt: string;\n}\n\n/** Private execution context, never included in the HTTP history summary. */\nexport interface SavedSessionExecution {\n cwd: string;\n repoRoot: string;\n workspaceId: string;\n groupingRoot: string;\n parentSessionId?: string;\n sessionProvenance?: string;\n interaction?: 'interactive' | 'unattended';\n}\n\nexport interface SavedSessionRecord {\n summary: SavedSession;\n execution: SavedSessionExecution;\n}\n\nexport function readSavedExecution(raw: unknown, owner: string): SavedSessionExecution | undefined {\n if (typeof raw !== 'string') return undefined;\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return undefined;\n }\n if (typeof parsed !== 'object' || parsed === null) return undefined;\n const record = parsed as Record<string, unknown>;\n if (\n typeof record.cwd !== 'string' ||\n !path.isAbsolute(record.cwd) ||\n typeof record.repoRoot !== 'string' ||\n !path.isAbsolute(record.repoRoot) ||\n record.repoRoot !== owner ||\n typeof record.workspaceId !== 'string' ||\n record.workspaceId === '' ||\n typeof record.groupingRoot !== 'string' ||\n !path.isAbsolute(record.groupingRoot)\n )\n return undefined;\n if (record.parentSessionId !== undefined && typeof record.parentSessionId !== 'string') return undefined;\n if (record.sessionProvenance !== undefined && typeof record.sessionProvenance !== 'string') return undefined;\n if (record.interaction !== undefined && record.interaction !== 'interactive' && record.interaction !== 'unattended')\n return undefined;\n return {\n cwd: record.cwd,\n repoRoot: record.repoRoot,\n workspaceId: record.workspaceId,\n groupingRoot: record.groupingRoot,\n ...(typeof record.parentSessionId === 'string' ? { parentSessionId: record.parentSessionId } : {}),\n ...(typeof record.sessionProvenance === 'string' ? { sessionProvenance: record.sessionProvenance } : {}),\n ...(record.interaction === undefined ? {} : { interaction: record.interaction }),\n };\n}\n\n/** The oldest user message of one conversation's fork-aware history older than `before`, read only up to it. */\nasync function firstUserEntry(\n storage: Storage,\n conversationId: ConversationId,\n before: EntryId | undefined,\n): Promise<{ id: EntryId; message: NonNullable<EntryRecord['model']>[number] } | undefined> {\n let cursor: Cursor | undefined;\n do {\n const page = await storage.scanEntries(\n { conversationId, order: 'ascending', ...(before === undefined ? {} : { maxEntryId: (before - 1) as EntryId }) },\n 100,\n cursor,\n context,\n );\n for (const entry of page.items) {\n const message = entry.model?.find((item) => item.role === 'user');\n if (message) return { id: entry.id, message };\n }\n cursor = page.next;\n } while (cursor);\n return undefined;\n}\n/**\n * Lists the stored sessions the injected storage holds, read-only. One file that cannot be read is\n * skipped and handed to `onUnreadable`, so it never empties the rest of the history.\n */\nexport async function listSavedSessionRecords(\n sessions: PoSessionStorageService,\n workspaceRoot: string,\n activeSessionIds: ReadonlySet<string>,\n workspaceId?: string,\n onUnreadable?: (sessionId: string, error: unknown) => void,\n): Promise<SavedSessionRecord[]> {\n const result: SavedSessionRecord[] = [];\n for (const { sessionId: id, updatedAt } of await sessions.list(context)) {\n if (activeSessionIds.has(id)) continue;\n let storage: Storage;\n try {\n storage = await sessions.openReadOnly(id, context);\n } catch (error) {\n onUnreadable?.(id, error);\n continue;\n }\n const session = createSession(storage);\n try {\n const identity = await session.snapshot(SessionIdentityDoc, context);\n if (!identity || identity.id !== id) throw new Error('Durable session identity mismatch');\n const metadata = await session.snapshot(SessionMetadataDoc, context);\n if (!metadata?.workspaceRoot) continue;\n const execution = readSavedExecution(metadata.execution, metadata.workspaceRoot);\n if (\n !execution ||\n execution.groupingRoot !== workspaceRoot ||\n (workspaceId && execution.workspaceId !== workspaceId)\n )\n continue;\n let first: Awaited<ReturnType<typeof firstUserEntry>>;\n let conversationCursor: Cursor | undefined;\n do {\n const conversations = await storage.scanConversations({}, 100, conversationCursor, context);\n for (const conversation of conversations.items) {\n // A task owns a child's conversation; the session's own history is its ownerless ones.\n if (conversation.owner) continue;\n first = (await firstUserEntry(storage, conversation.id, first?.id)) ?? first;\n }\n conversationCursor = conversations.next;\n } while (conversationCursor);\n const content = first?.message.content;\n const firstMessage =\n typeof content === 'string'\n ? content\n : Array.isArray(content)\n ? content\n .filter((part) => part.type === 'text')\n .map((part) => part.text)\n .join('\\n')\n : '';\n result.push({\n summary: {\n id,\n ...(metadata.name ? { name: metadata.name } : {}),\n firstMessage,\n createdAt: new Date(identity.createdAt).toISOString(),\n updatedAt: new Date(updatedAt).toISOString(),\n },\n execution,\n });\n } catch (error) {\n onUnreadable?.(id, error);\n } finally {\n await session.close(context);\n }\n }\n return result.sort((a, b) => b.summary.updatedAt.localeCompare(a.summary.updatedAt));\n}\n\n/** Public summaries do not expose private execution metadata. */\nexport async function listSavedSessions(\n sessions: PoSessionStorageService,\n workspaceRoot: string,\n activeSessionIds: ReadonlySet<string>,\n workspaceId?: string,\n onUnreadable?: (sessionId: string, error: unknown) => void,\n): Promise<SavedSession[]> {\n return (await listSavedSessionRecords(sessions, workspaceRoot, activeSessionIds, workspaceId, onUnreadable)).map(\n (record) => record.summary,\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":["context"],"sources":["../../../../src/services/sqliteSessionHistory/index.ts"],"sourcesContent":["import path from 'node:path';\n\nimport {\n createSession,\n type ConversationId,\n type Cursor,\n type EntryId,\n type EntryRecord,\n type Storage,\n} from '@earendil-works/pi-durable';\n\nimport type { PoSessionStorageService } from '../../schemas/platform';\nimport { PO_CONTEXT_ENTRY_TYPE, readProjectedSelection, type ContextProjection } from '../contextProjection';\nimport { DurableNavigationDoc } from '../durableNavigation/docs';\nimport { DURABLE_BACKGROUND_CONTEXT as context, SessionIdentityDoc, SessionMetadataDoc } from '../sqliteSessionStorage';\nimport { findNewestDurableEntry } from '../transcriptPages';\n\nexport interface SavedSession {\n id: string;\n name?: string;\n firstMessage: string;\n createdAt: string;\n updatedAt: string;\n}\n\n/** A session hidden from History that can still be restored until `purgeAt` (ISO 8601). */\nexport interface RemovedSession extends SavedSession {\n removedAt: string;\n purgeAt: string;\n}\n\n/** Private execution context, never included in the HTTP history summary. */\nexport interface SavedSessionExecution {\n cwd: string;\n repoRoot: string;\n workspaceId: string;\n groupingRoot: string;\n parentSessionId?: string;\n sessionProvenance?: string;\n interaction?: 'interactive' | 'unattended';\n}\n\nexport interface SavedSessionRecord {\n summary: SavedSession;\n execution: SavedSessionExecution;\n}\n\nexport function readSavedExecution(raw: unknown, owner: string): SavedSessionExecution | undefined {\n if (typeof raw !== 'string') return undefined;\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return undefined;\n }\n if (typeof parsed !== 'object' || parsed === null) return undefined;\n const record = parsed as Record<string, unknown>;\n if (\n typeof record.cwd !== 'string' ||\n !path.isAbsolute(record.cwd) ||\n typeof record.repoRoot !== 'string' ||\n !path.isAbsolute(record.repoRoot) ||\n record.repoRoot !== owner ||\n typeof record.workspaceId !== 'string' ||\n record.workspaceId === '' ||\n typeof record.groupingRoot !== 'string' ||\n !path.isAbsolute(record.groupingRoot)\n )\n return undefined;\n if (record.parentSessionId !== undefined && typeof record.parentSessionId !== 'string') return undefined;\n if (record.sessionProvenance !== undefined && typeof record.sessionProvenance !== 'string') return undefined;\n if (record.interaction !== undefined && record.interaction !== 'interactive' && record.interaction !== 'unattended')\n return undefined;\n return {\n cwd: record.cwd,\n repoRoot: record.repoRoot,\n workspaceId: record.workspaceId,\n groupingRoot: record.groupingRoot,\n ...(typeof record.parentSessionId === 'string' ? { parentSessionId: record.parentSessionId } : {}),\n ...(typeof record.sessionProvenance === 'string' ? { sessionProvenance: record.sessionProvenance } : {}),\n ...(record.interaction === undefined ? {} : { interaction: record.interaction }),\n };\n}\n\n/** The oldest user message of one conversation's fork-aware history older than `before`, read only up to it. */\nasync function firstUserEntry(\n storage: Storage,\n conversationId: ConversationId,\n before: EntryId | undefined,\n): Promise<{ id: EntryId; message: NonNullable<EntryRecord['model']>[number] } | undefined> {\n let cursor: Cursor | undefined;\n do {\n const page = await storage.scanEntries(\n { conversationId, order: 'ascending', ...(before === undefined ? {} : { maxEntryId: (before - 1) as EntryId }) },\n 100,\n cursor,\n context,\n );\n for (const entry of page.items) {\n const message = entry.model?.find((item) => item.role === 'user');\n if (message) return { id: entry.id, message };\n }\n cursor = page.next;\n } while (cursor);\n return undefined;\n}\n/**\n * The selection a stored session last projected into its active conversation, read-only. The newest valid\n * projection wins even when it names no profile: that absence is the saved answer, not a gap to fill from\n * an older projection. A fork reads its own history, not the abandoned tail of its source.\n */\nexport async function readSavedSelection(\n sessions: PoSessionStorageService,\n sessionId: string,\n): Promise<ContextProjection['selection'] | undefined> {\n const storage = await sessions.openReadOnly(sessionId, context);\n const session = createSession(storage);\n try {\n const identity = await session.snapshot(SessionIdentityDoc, context);\n if (!identity || identity.id !== sessionId) throw new Error('Durable session identity mismatch');\n const conversationId = (await session.snapshot(DurableNavigationDoc, context))?.activeConversationId;\n if (conversationId === null || conversationId === undefined) return undefined;\n const projected = await findNewestDurableEntry(\n (query, limit, cursor, scanContext) =>\n storage.scanEntries({ conversationId, ...query }, limit, cursor, scanContext),\n context,\n (entry) =>\n entry.type === 'custom' &&\n entry.customType === PO_CONTEXT_ENTRY_TYPE &&\n readProjectedSelection(entry.data) !== undefined,\n );\n return projected?.type === 'custom' ? readProjectedSelection(projected.data) : undefined;\n } finally {\n await session.close(context);\n }\n}\n\n/**\n * Lists the stored sessions the injected storage holds, read-only. One file that cannot be read is\n * skipped and handed to `onUnreadable`, so it never empties the rest of the history.\n */\nexport async function listSavedSessionRecords(\n sessions: PoSessionStorageService,\n workspaceRoot: string,\n activeSessionIds: ReadonlySet<string>,\n workspaceId?: string,\n onUnreadable?: (sessionId: string, error: unknown) => void,\n): Promise<SavedSessionRecord[]> {\n const result: SavedSessionRecord[] = [];\n for (const { sessionId: id, updatedAt } of await sessions.list(context)) {\n if (activeSessionIds.has(id)) continue;\n let storage: Storage;\n try {\n storage = await sessions.openReadOnly(id, context);\n } catch (error) {\n onUnreadable?.(id, error);\n continue;\n }\n const session = createSession(storage);\n try {\n const identity = await session.snapshot(SessionIdentityDoc, context);\n if (!identity || identity.id !== id) throw new Error('Durable session identity mismatch');\n const metadata = await session.snapshot(SessionMetadataDoc, context);\n if (!metadata?.workspaceRoot) continue;\n const execution = readSavedExecution(metadata.execution, metadata.workspaceRoot);\n if (\n !execution ||\n execution.groupingRoot !== workspaceRoot ||\n (workspaceId && execution.workspaceId !== workspaceId)\n )\n continue;\n let first: Awaited<ReturnType<typeof firstUserEntry>>;\n let conversationCursor: Cursor | undefined;\n do {\n const conversations = await storage.scanConversations({}, 100, conversationCursor, context);\n for (const conversation of conversations.items) {\n // A task owns a child's conversation; the session's own history is its ownerless ones.\n if (conversation.owner) continue;\n first = (await firstUserEntry(storage, conversation.id, first?.id)) ?? first;\n }\n conversationCursor = conversations.next;\n } while (conversationCursor);\n const content = first?.message.content;\n const firstMessage =\n typeof content === 'string'\n ? content\n : Array.isArray(content)\n ? content\n .filter((part) => part.type === 'text')\n .map((part) => part.text)\n .join('\\n')\n : '';\n result.push({\n summary: {\n id,\n ...(metadata.name ? { name: metadata.name } : {}),\n firstMessage,\n createdAt: new Date(identity.createdAt).toISOString(),\n updatedAt: new Date(updatedAt).toISOString(),\n },\n execution,\n });\n } catch (error) {\n onUnreadable?.(id, error);\n } finally {\n await session.close(context);\n }\n }\n return result.sort((a, b) => b.summary.updatedAt.localeCompare(a.summary.updatedAt));\n}\n\n/** Public summaries do not expose private execution metadata. */\nexport async function listSavedSessions(\n sessions: PoSessionStorageService,\n workspaceRoot: string,\n activeSessionIds: ReadonlySet<string>,\n workspaceId?: string,\n onUnreadable?: (sessionId: string, error: unknown) => void,\n): Promise<SavedSession[]> {\n return (await listSavedSessionRecords(sessions, workspaceRoot, activeSessionIds, workspaceId, onUnreadable)).map(\n (record) => record.summary,\n );\n}\n"],"mappings":"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"}