@factory/droid-sdk 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1874,8 +1874,6 @@ var ModelID = /* @__PURE__ */ ((ModelID2) => {
1874
1874
  ModelID2["CLAUDE_FABLE_5"] = "claude-fable-5";
1875
1875
  ModelID2["ATLAS_0721"] = "atlas-07-21";
1876
1876
  ModelID2["ASTER_0715"] = "aster-07-15";
1877
- ModelID2["AMBER_0709"] = "amber-07-09";
1878
- ModelID2["AGATE_0711"] = "agate-07-11";
1879
1877
  ModelID2["GPT_5"] = "gpt-5-2025-08-07";
1880
1878
  ModelID2["GPT_5_MINI"] = "gpt-5-mini-2025-08-07";
1881
1879
  ModelID2["GPT_5_NANO"] = "gpt-5-nano-2025-08-07";
@@ -1928,6 +1926,8 @@ var ModelID = /* @__PURE__ */ ((ModelID2) => {
1928
1926
  ModelID2["GLM_5_1"] = "glm-5.1";
1929
1927
  ModelID2["GLM_5_2"] = "glm-5.2";
1930
1928
  ModelID2["GLM_5_2_FAST"] = "glm-5.2-fast";
1929
+ ModelID2["GLM_5_3"] = "glm-5.3";
1930
+ ModelID2["GLM_5_3_FLASH"] = "glm-5.3-flash";
1931
1931
  ModelID2["INKLING"] = "inkling";
1932
1932
  ModelID2["NEMOTRON_3_ULTRA"] = "nemotron-3-ultra";
1933
1933
  ModelID2["SHIELD_RISK"] = "shield-risk";
@@ -2052,11 +2052,36 @@ var BugReportSourceSchema = z.object({
2052
2052
  osVersion: z.string().max(100).optional()
2053
2053
  });
2054
2054
  var ToolExecutionModeSchema = z.nativeEnum(ToolExecutionMode);
2055
+ var SdkClientMetadataSchema = z.object({
2056
+ language: z.enum(["typescript", "python"]),
2057
+ version: z.string().min(1).max(64).regex(/^[A-Za-z0-9.+-]+$/)
2058
+ });
2059
+ function formatSdkClientMetadata(metadata) {
2060
+ return `${metadata.language}/${metadata.version}`;
2061
+ }
2062
+ var ClientRequestAttributionSchema = z.discriminatedUnion("client", [
2063
+ z.object({
2064
+ client: z.literal("sdk" /* SDK */),
2065
+ sdk: SdkClientMetadataSchema
2066
+ }),
2067
+ z.object({
2068
+ client: z.enum([
2069
+ "web-desktop" /* WebDesktop */,
2070
+ "web-app" /* WebApp */,
2071
+ "web-workspace" /* WebWorkspace */,
2072
+ "daemon" /* Daemon */,
2073
+ "cli" /* CLI */,
2074
+ "backend" /* Backend */
2075
+ ])
2076
+ })
2077
+ ]);
2055
2078
  var TraceContextMetaSchema = z.object({
2056
2079
  /** W3C traceparent header value (e.g., "00-0af7651916cd43dd8448eb211c80319c-00f067aa0ba902b7-01") */
2057
2080
  traceparent: z.string().optional(),
2058
2081
  /** W3C tracestate header value for vendor-specific trace data */
2059
- tracestate: z.string().optional()
2082
+ tracestate: z.string().optional(),
2083
+ /** Authenticated caller attribution injected by a daemon before worker dispatch. */
2084
+ requestAttribution: ClientRequestAttributionSchema.optional()
2060
2085
  });
2061
2086
  var JsonRpcEnvelopeSchema = z.object({
2062
2087
  jsonrpc: z.literal(JSONRPC_VERSION),
@@ -2511,6 +2536,17 @@ var ListModelsOptionsSchema = z5.object({
2511
2536
  var ListModelsResultSchema = z5.object({
2512
2537
  models: z5.array(ModelInfoSchema)
2513
2538
  });
2539
+ var ModelFallbackReasonSchema = z5.enum([
2540
+ /** A BYOK `custom:*` id with no matching entry in the machine's settings. */
2541
+ "custom_model_not_configured",
2542
+ /** The model is no longer available to the org that requested it. */
2543
+ "model_not_available_for_org"
2544
+ ]);
2545
+ var ModelFallbackSchema = z5.object({
2546
+ /** The unusable id, as requested. */
2547
+ requestedModel: z5.string(),
2548
+ reason: ModelFallbackReasonSchema
2549
+ });
2514
2550
 
2515
2551
  // ../droid-sdk-core/src/protocol/droid/schemas/mission-decomposition.ts
2516
2552
  import z6 from "zod";
@@ -2808,7 +2844,9 @@ var LEGACY_COMPACTION_MODEL_IDS = [
2808
2844
  "oxide-06-01",
2809
2845
  "oxbow-06-01",
2810
2846
  "owl-06-21",
2811
- "gantry-05-07"
2847
+ "gantry-05-07",
2848
+ "amber-07-09",
2849
+ "agate-07-11"
2812
2850
  ];
2813
2851
  var SHIELD_MODEL_ID_VALUES_SET = new Set(
2814
2852
  SHIELD_MODEL_ID_VALUES
@@ -2878,7 +2916,10 @@ var McpOAuthConfigSchema = z10.union([
2878
2916
  McpOAuthOptionsSchema
2879
2917
  ]);
2880
2918
  var BuiltInModelIDSchema = z10.custom(
2881
- (value) => typeof value === "string" && Object.values(ModelID).some((modelId) => modelId === value) && !ROUTER_MODEL_IDS.some((routerId) => routerId === value)
2919
+ (value) => typeof value === "string" && Object.values(ModelID).some((modelId) => modelId === value) && !ROUTER_MODEL_IDS.some((routerId) => routerId === value),
2920
+ {
2921
+ message: "Must be a supported built-in model ID."
2922
+ }
2882
2923
  );
2883
2924
  var ENV_VAR_REFERENCE_PATTERN = /\$\{[A-Za-z_][A-Za-z0-9_]*\}/;
2884
2925
  var CustomModelBedrockRequestMetadataSchema = z10.record(
@@ -3818,8 +3859,8 @@ var InitializeSessionRequestParamsSchema = z14.object({
3818
3859
  sessionOriginHint: z14.nativeEnum(SessionOrigin).optional(),
3819
3860
  tags: z14.array(SessionTagSchema).optional(),
3820
3861
  privacyLevel: z14.enum(["private", "organization"]).optional(),
3821
- // Retained for wire compatibility. Organization-scoped local session
3822
- // behavior was reverted, so consumers intentionally ignore this value.
3862
+ // Active organization that owns the local session. The daemon validates
3863
+ // this against the authenticated connection before persisting it.
3823
3864
  organizationId: z14.string().optional(),
3824
3865
  title: z14.string().optional(),
3825
3866
  /**
@@ -3886,6 +3927,12 @@ var InitializeSessionRequestParamsSchema = z14.object({
3886
3927
  * (`<current-branch>-wt`) is preserved.
3887
3928
  */
3888
3929
  worktreeBaseBranch: WorktreeGitRefSchema.optional(),
3930
+ /**
3931
+ * Whether `worktreeBaseBranch` was selected from origin before it existed
3932
+ * locally. The daemon fetches it and creates or copies it only when the
3933
+ * session starts.
3934
+ */
3935
+ worktreeBaseBranchFromOrigin: z14.boolean().optional(),
3889
3936
  /**
3890
3937
  * Explicit branch name to create for the worktree, chosen by the user
3891
3938
  * through the branch selector's "Create branch" action. When provided,
@@ -3903,7 +3950,12 @@ var InitializeSessionRequestParamsSchema = z14.object({
3903
3950
  * ID of the setup profile to execute in the worktree after creation.
3904
3951
  * Must match a profile stored on the target daemon for the project.
3905
3952
  */
3906
- worktreeSetupProfileId: z14.string().uuid().optional()
3953
+ worktreeSetupProfileId: z14.string().uuid().optional(),
3954
+ /** Cross-machine parent lineage supplied by a trusted daemon. */
3955
+ callingMetadata: z14.object({
3956
+ callingSessionId: z14.string().min(1),
3957
+ callingToolUseId: z14.string().min(1)
3958
+ }).optional()
3907
3959
  }).merge(ToolOverrideParamsSchema);
3908
3960
  var LoadSessionRequestParamsSchema = z14.object({
3909
3961
  sessionId: z14.string(),
@@ -3940,6 +3992,7 @@ var LoadSessionRequestParamsSchema = z14.object({
3940
3992
  var AddUserMessageRequestParamsSchema = z14.object({
3941
3993
  messageId: z14.string().optional(),
3942
3994
  text: z14.string(),
3995
+ content: z14.array(z14.union([TextBlockSchema, ImageBlockSchema])).min(1).optional(),
3943
3996
  images: Base64ImageSourceSchema.array().optional(),
3944
3997
  imagePaths: z14.string().array().optional(),
3945
3998
  files: DocumentSourceSchema.array().optional(),
@@ -4906,7 +4959,8 @@ var DaemonConnectionMetadataSchema = z15.object({
4906
4959
  connectAttemptId: z15.string().optional(),
4907
4960
  /** Low-cardinality trigger for the root computer-connect episode. */
4908
4961
  connectAttemptTrigger: z15.string().optional()
4909
- }).optional()
4962
+ }).optional(),
4963
+ sdk: SdkClientMetadataSchema.optional()
4910
4964
  });
4911
4965
  var DaemonAuthenticateRequestParamsSchema = z15.object({
4912
4966
  /** WorkOS JWT access token */
@@ -4930,7 +4984,13 @@ var DaemonAuthenticateRequestParamsSchema = z15.object({
4930
4984
  message: "Either token or apiKey must be provided"
4931
4985
  }).refine((data) => !data.actAsGrant || Boolean(data.token), {
4932
4986
  message: "actAsGrant requires token"
4933
- });
4987
+ }).refine(
4988
+ (data) => data.caller === "sdk" /* SDK */ === (data.metadata?.sdk !== void 0),
4989
+ {
4990
+ message: "SDK callers must provide SDK metadata",
4991
+ path: ["metadata", "sdk"]
4992
+ }
4993
+ );
4934
4994
  var DaemonAuthenticateRequestSchema = JsonRpcBaseRequestSchema.extend({
4935
4995
  method: z15.literal("daemon.authenticate" /* AUTHENTICATE */),
4936
4996
  params: DaemonAuthenticateRequestParamsSchema
@@ -5427,8 +5487,9 @@ function settleToolExecutionLifecyclePhase(phase) {
5427
5487
  }
5428
5488
 
5429
5489
  // ../droid-sdk-core/src/protocol/droid/schemas/constants.ts
5430
- var FACTORY_PROTOCOL_VERSION = "1.187.0";
5490
+ var FACTORY_PROTOCOL_VERSION = "1.193.0";
5431
5491
  var FACTORY_CLIENT_HEADER = "X-Factory-Client";
5492
+ var FACTORY_SDK_HEADER = "X-Factory-Sdk";
5432
5493
  var FACTORY_CLIENT_VERSION = "X-Client-Version";
5433
5494
  var LEGACY_FACTORY_API_VERSION2 = LEGACY_FACTORY_API_VERSION;
5434
5495
  var JSONRPC_VERSION2 = JSONRPC_VERSION;
@@ -5820,7 +5881,19 @@ var DaemonRunAutomationResultSchema = z20.object({
5820
5881
  /** Template this automation was created from, when known. */
5821
5882
  templateId: z20.nativeEnum(AutomationTemplateId).optional(),
5822
5883
  cwd: z20.string(),
5884
+ /**
5885
+ * The model to run on. Omitted when the automation pins no model, and also
5886
+ * when its configured model could not be resolved on this machine, in which
5887
+ * case `modelFallback` explains the omission and the run uses the daemon
5888
+ * default.
5889
+ */
5823
5890
  model: z20.string().optional(),
5891
+ /**
5892
+ * Set when the automation's configured model was dropped because it does not
5893
+ * resolve on this machine. The run still proceeds on the daemon default;
5894
+ * callers surface this so the substitution is not silent.
5895
+ */
5896
+ modelFallback: ModelFallbackSchema.optional(),
5824
5897
  /** Optional reasoning effort for the run session. */
5825
5898
  reasoningEffort: z20.string().optional(),
5826
5899
  /**
@@ -5894,7 +5967,13 @@ var AutomationRunRecordSchema = z20.object({
5894
5967
  isRetry: z20.boolean().optional(),
5895
5968
  originalRunId: z20.string().optional(),
5896
5969
  /** Droid session that executed this run, when dispatched as a real session. */
5897
- sessionId: z20.string().optional()
5970
+ sessionId: z20.string().optional(),
5971
+ /**
5972
+ * Set when this run's configured model was dropped at spawn time. Scheduled
5973
+ * runs fire without a caller to receive the run-time result, so the history
5974
+ * record is the only place the substitution can surface for them.
5975
+ */
5976
+ modelFallback: ModelFallbackSchema.optional()
5898
5977
  });
5899
5978
  var DaemonGetAutomationHistoryResultSchema = z20.object({
5900
5979
  automationId: z20.string(),
@@ -6906,6 +6985,7 @@ var SoftwareFactoryWorkstreamStateSchema = z28.enum([
6906
6985
  "draft",
6907
6986
  "active",
6908
6987
  "paused",
6988
+ // Retained for wire compatibility with older clients. New writes archive.
6909
6989
  "canceled",
6910
6990
  "archived"
6911
6991
  ]);
@@ -6940,12 +7020,15 @@ var SoftwareFactoryWorkstreamSchema = z28.object({
6940
7020
  icon: z28.string().optional(),
6941
7021
  state: SoftwareFactoryWorkstreamStateSchema,
6942
7022
  userId: z28.string().optional(),
6943
- /** Owning team when the workstream is team-owned; absent otherwise. */
7023
+ /**
7024
+ * Team the workstream is attributed to. A label, not a visibility control:
7025
+ * every workstream is readable by its whole org.
7026
+ */
6944
7027
  owningTeamId: z28.string().optional(),
6945
7028
  /**
6946
- * Access mode gating human reads/writes: `team` (owning-team members),
6947
- * `private` (creator only). Absent on legacy rows, which are treated as
6948
- * open.
7029
+ * Retired: workstreams are org-visible, so the backend no longer reports an
7030
+ * access mode. Present only on rows written before the mode was retired, and
7031
+ * kept in the schema because the protocol forbids removing a field.
6949
7032
  */
6950
7033
  accessMode: z28.enum(["team", "private"]).optional(),
6951
7034
  creationSessionId: z28.string().optional(),
@@ -6964,6 +7047,9 @@ var SoftwareFactoryWorkstreamSchema = z28.object({
6964
7047
  dashboardPort: z28.number().int().optional(),
6965
7048
  /** Public URL of the workstream's published Conjure dashboard site. */
6966
7049
  dashboardUrl: z28.string().optional(),
7050
+ /** Pause proposed changes for human approval before investigation. */
7051
+ requireChangeApproval: z28.boolean().optional(),
7052
+ /** Pause completed investigation specifications for human approval. */
6967
7053
  requireApproval: z28.boolean(),
6968
7054
  reviewers: z28.array(z28.string()),
6969
7055
  createdAt: z28.string(),
@@ -7005,7 +7091,7 @@ var SoftwareFactoryActivitySchema = z28.object({
7005
7091
  id: z28.string(),
7006
7092
  changeId: z28.string(),
7007
7093
  workstreamId: z28.string(),
7008
- kind: z28.enum(["investigation", "implementation", "steward"]),
7094
+ kind: z28.enum(["change_review", "investigation", "implementation", "steward"]),
7009
7095
  status: z28.enum(["pending", "in_progress", "completed", "failed", "canceled"]),
7010
7096
  reviewStatus: z28.enum(["awaiting", "approved", "declined", "commented"]).optional(),
7011
7097
  reviewComment: z28.string().optional(),
@@ -7087,15 +7173,15 @@ var DaemonSfCreateWorkstreamRequestParamsSchema = z28.object({
7087
7173
  */
7088
7174
  userId: z28.string().optional(),
7089
7175
  /**
7090
- * Owning team for a team-owned workstream. When set, the backend validates
7091
- * the acting principal may create for this team, stamps `access_mode='team'`,
7092
- * and issues the owner grant.
7176
+ * Team to attribute the workstream to. When set, the backend validates the
7177
+ * acting principal is linked to this team, stamps it, and issues the owner
7178
+ * grant. It does not restrict who can read the workstream.
7093
7179
  */
7094
7180
  owningTeamId: z28.string().optional(),
7095
7181
  /**
7096
- * Omitting the access mode leaves the workstream legacy-open (org-visible);
7097
- * it is not defaulted to `private` server-side. Set `team`/`private` to
7098
- * enforce access.
7182
+ * Accepted and ignored: workstreams are visible to the whole org, so the
7183
+ * daemon drops this rather than forwarding it. Kept in the schema because the
7184
+ * protocol forbids removing a field, and so an older client still validates.
7099
7185
  */
7100
7186
  accessMode: z28.enum(["team", "private"]).optional(),
7101
7187
  /** Creation session that owns the bootstrap interview. */
@@ -7106,6 +7192,7 @@ var DaemonSfCreateWorkstreamRequestParamsSchema = z28.object({
7106
7192
  repos: z28.array(z28.string()).optional(),
7107
7193
  workerConcurrency: z28.number().optional(),
7108
7194
  stewardConcurrency: z28.number().optional(),
7195
+ requireChangeApproval: z28.boolean().optional(),
7109
7196
  requireApproval: z28.boolean().optional(),
7110
7197
  reviewers: z28.array(z28.string()).optional()
7111
7198
  });
@@ -7138,6 +7225,7 @@ var DaemonSfUpdateWorkstreamRequestParamsSchema = z28.object({
7138
7225
  stewardConcurrency: z28.number().optional(),
7139
7226
  dashboardPort: z28.number().optional(),
7140
7227
  dashboardUrl: z28.string().optional(),
7228
+ requireChangeApproval: z28.boolean().optional(),
7141
7229
  requireApproval: z28.boolean().optional(),
7142
7230
  reviewers: z28.array(z28.string()).optional()
7143
7231
  });
@@ -7162,7 +7250,7 @@ var DaemonSfDeleteWorkstreamResultSchema = z28.object({
7162
7250
  automationsDeleted: z28.number(),
7163
7251
  /** Whether the workstream directory existed and was removed. */
7164
7252
  workstreamDirDeleted: z28.boolean(),
7165
- /** Number of remote (backend-scheduled) automations tagged to the workstream that were soft-deleted. */
7253
+ /** Number of linked remote (backend-scheduled) automations that were soft-deleted. */
7166
7254
  remoteAutomationsDeleted: z28.number().optional()
7167
7255
  });
7168
7256
  var DaemonSfListEventsRequestParamsSchema = z28.object({
@@ -7595,7 +7683,9 @@ var DaemonInitializeSessionRequestParamsSchema = InitializeSessionRequestParamsS
7595
7683
  * message so worktree creation can honour it: a worktree that cuts a new
7596
7684
  * branch from its base starts from the base's freshly fetched remote ref
7597
7685
  * instead of the possibly stale local one. The pull that mutates an
7598
- * existing checkout still rides on the first message, not on init.
7686
+ * existing checkout still rides on the first message, not on init. This
7687
+ * controls an update from origin; origin-only checkout creation uses
7688
+ * `createFromOrigin` on the checkout request instead.
7599
7689
  */
7600
7690
  pullFromOrigin: z31.boolean().optional()
7601
7691
  });
@@ -7618,7 +7708,8 @@ var DaemonAddUserMessageRequestParamsSchema = AddUserMessageRequestParamsSchema.
7618
7708
  * this message to the agent. Set by session-launch callers on the first
7619
7709
  * message only; a failed pull is not fatal, it arrives as its own
7620
7710
  * LLM-only message ahead of this one so the agent resolves it without the
7621
- * user's own text being touched.
7711
+ * user's own text being touched. This updates the checked-out branch; it
7712
+ * does not create an origin-only branch locally.
7622
7713
  */
7623
7714
  pullFromOrigin: z31.boolean().optional()
7624
7715
  });
@@ -7789,6 +7880,7 @@ var DaemonAvailableSessionInfoSchema = z31.object({
7789
7880
  // Parent session ID for child/worker sessions (from JSONL summary `callingSessionId` field)
7790
7881
  callingToolUseId: z31.string().optional(),
7791
7882
  // Parent tool use ID that spawned this child session (from JSONL summary)
7883
+ organizationId: z31.string().optional(),
7792
7884
  tags: z31.array(SessionTagSchema).optional(),
7793
7885
  // Session tags for categorization
7794
7886
  // Drop only the worktree metadata (not the whole session, and not the whole
@@ -8031,11 +8123,41 @@ var DaemonRenameSessionRequestSchema = JsonRpcBaseRequestSchema.extend(
8031
8123
  );
8032
8124
  var DaemonRenameSessionResponseSchema = createAckCompatibleResponseSchema(DaemonRenameSessionResultSchema);
8033
8125
  var DaemonListGitBranchesRequestParamsSchema = z31.object({
8034
- cwd: z31.string()
8126
+ cwd: z31.string(),
8127
+ /**
8128
+ * Whether to discover branches that exist only on origin. Optional so newer
8129
+ * clients can continue listing local branches through older request shapes.
8130
+ */
8131
+ includeOriginBranches: z31.boolean().optional()
8132
+ });
8133
+ var DaemonCheckoutGitBranchConflictDataSchema = z31.object({
8134
+ reason: z31.literal("branch_occupied_by_managed_worktree"),
8135
+ worktree: z31.discriminatedUnion("lifecycle", [
8136
+ SessionWorktreeMetadataSchema.pick({
8137
+ path: true,
8138
+ repoRoot: true
8139
+ }).required().extend({
8140
+ lifecycle: z31.literal("ephemeral" /* Ephemeral */),
8141
+ sessionId: z31.string()
8142
+ }),
8143
+ SessionWorktreeMetadataSchema.pick({
8144
+ path: true,
8145
+ repoRoot: true
8146
+ }).required().extend({
8147
+ lifecycle: z31.literal("persistent" /* Persistent */)
8148
+ })
8149
+ ])
8035
8150
  });
8036
8151
  var DaemonListGitBranchesResultSchema = z31.object({
8037
8152
  isGitRepository: z31.boolean(),
8038
8153
  branches: z31.array(z31.string()),
8154
+ /**
8155
+ * Branches found on origin that do not yet exist locally. Optional for
8156
+ * compatibility with daemons predating remote branch discovery.
8157
+ */
8158
+ originBranches: z31.array(z31.string()).optional(),
8159
+ /** Whether origin branch discovery is still refreshing in the background. */
8160
+ originBranchesPending: z31.boolean().optional(),
8039
8161
  currentBranch: z31.string().nullable(),
8040
8162
  /** The repository's default branch, when it has one. */
8041
8163
  defaultBranch: z31.string().optional(),
@@ -8046,7 +8168,9 @@ var DaemonListGitBranchesResultSchema = z31.object({
8046
8168
  checkedOutBranches: z31.array(
8047
8169
  z31.object({
8048
8170
  branch: z31.string(),
8049
- path: z31.string()
8171
+ path: z31.string(),
8172
+ /** Branch-switch conflict details for a Factory-managed worktree. */
8173
+ conflict: DaemonCheckoutGitBranchConflictDataSchema.optional()
8050
8174
  })
8051
8175
  ).optional()
8052
8176
  });
@@ -8061,32 +8185,26 @@ var DaemonCheckoutGitBranchRequestParamsSchema = z31.object({
8061
8185
  cwd: z31.string(),
8062
8186
  branch: z31.string(),
8063
8187
  create: z31.boolean().optional(),
8188
+ /**
8189
+ * Create a local tracking branch from the same-named origin branch. This
8190
+ * describes where the local branch comes from; it does not request a pull.
8191
+ */
8192
+ createFromOrigin: z31.boolean().optional(),
8193
+ /**
8194
+ * Pull the newly checked-out branch before returning. For an origin-only
8195
+ * selection, use this with `createFromOrigin`.
8196
+ */
8197
+ pullFromOrigin: z31.boolean().optional(),
8064
8198
  resolution: z31.enum(["stash", "commit"]).optional()
8065
8199
  });
8066
- var DaemonCheckoutGitBranchConflictDataSchema = z31.object({
8067
- reason: z31.literal("branch_occupied_by_managed_worktree"),
8068
- worktree: z31.discriminatedUnion("lifecycle", [
8069
- SessionWorktreeMetadataSchema.pick({
8070
- path: true,
8071
- repoRoot: true
8072
- }).required().extend({
8073
- lifecycle: z31.literal("ephemeral" /* Ephemeral */),
8074
- sessionId: z31.string()
8075
- }),
8076
- SessionWorktreeMetadataSchema.pick({
8077
- path: true,
8078
- repoRoot: true
8079
- }).required().extend({
8080
- lifecycle: z31.literal("persistent" /* Persistent */)
8081
- })
8082
- ])
8083
- });
8084
8200
  var DaemonCheckoutGitBranchResultSchema = z31.discriminatedUnion(
8085
8201
  "status",
8086
8202
  [
8087
8203
  z31.object({
8088
8204
  status: z31.literal("checked_out"),
8089
- currentBranch: z31.string()
8205
+ currentBranch: z31.string(),
8206
+ /** Scrubbed git output when an active-session origin pull failed. */
8207
+ pullFailure: z31.string().optional()
8090
8208
  }),
8091
8209
  z31.object({
8092
8210
  status: z31.literal("needs_resolution"),
@@ -9181,10 +9299,18 @@ var SESSION_INIT_TIMEOUT = 6e4;
9181
9299
  var MCP_AUTH_TIMEOUT = 3e5;
9182
9300
  var COMPACTION_TIMEOUT = 24e4;
9183
9301
  var REWIND_TIMEOUT = 6e4;
9184
- var SDK_VERSION = "0.8.0";
9302
+ var SDK_VERSION = "0.9.0";
9303
+ var SDK_CLIENT_METADATA = {
9304
+ language: "typescript",
9305
+ version: SDK_VERSION
9306
+ };
9307
+ var SDK_REQUEST_ATTRIBUTION = {
9308
+ client: "sdk" /* SDK */,
9309
+ sdk: SDK_CLIENT_METADATA
9310
+ };
9185
9311
  var SDK_TAG = {
9186
9312
  name: "sdk",
9187
- metadata: { language: "typescript", version: SDK_VERSION }
9313
+ metadata: SDK_CLIENT_METADATA
9188
9314
  };
9189
9315
 
9190
9316
  // src/schemas/constants.ts
@@ -9343,6 +9469,9 @@ function mergeSessionSettings(current, patch) {
9343
9469
  }
9344
9470
  return SessionSettingsSchema2.parse(merged);
9345
9471
  }
9472
+ function withSdkTag(tags = []) {
9473
+ return [...tags.filter((tag) => tag.name !== SDK_TAG.name), SDK_TAG];
9474
+ }
9346
9475
  function unwrapResponseResult(response) {
9347
9476
  if (response.error) {
9348
9477
  throw new ProtocolError(response.error.message, {
@@ -10164,21 +10293,27 @@ function isPendingToolResultMarker(block) {
10164
10293
  }
10165
10294
  function buildUserMessageContentBlocks({
10166
10295
  text,
10296
+ content,
10167
10297
  images,
10168
10298
  files,
10169
10299
  trimText = false,
10170
10300
  includeEmptyText = true
10171
10301
  }) {
10172
- const blocks = [];
10173
- for (const image of images ?? []) {
10174
- blocks.push({ type: "image" /* Image */, source: image });
10302
+ const blocks = content?.length ? [...content] : [];
10303
+ const hasExplicitContent = blocks.length > 0;
10304
+ if (!hasExplicitContent) {
10305
+ for (const image of images ?? []) {
10306
+ blocks.push({ type: "image" /* Image */, source: image });
10307
+ }
10175
10308
  }
10176
10309
  for (const file of files ?? []) {
10177
10310
  blocks.push({ type: "document" /* Document */, source: file });
10178
10311
  }
10179
- const value = trimText ? text?.trim() : text;
10180
- if (value !== void 0 && (includeEmptyText || value.length > 0)) {
10181
- blocks.push({ type: "text" /* Text */, text: value });
10312
+ if (!hasExplicitContent) {
10313
+ const value = trimText ? text?.trim() : text;
10314
+ if (value !== void 0 && (includeEmptyText || value.length > 0)) {
10315
+ blocks.push({ type: "text" /* Text */, text: value });
10316
+ }
10182
10317
  }
10183
10318
  return blocks;
10184
10319
  }
@@ -12856,7 +12991,8 @@ async function connectWithRetry({
12856
12991
  ensureRunning,
12857
12992
  providerType,
12858
12993
  maxRetries = DEFAULT_MAX_RETRIES,
12859
- connectAttemptContext
12994
+ connectAttemptContext,
12995
+ onAttempts
12860
12996
  }) {
12861
12997
  if (ensureRunning) {
12862
12998
  await OtelTracing.trace("computer.ensure_running" /* COMPUTER_ENSURE_RUNNING */, ensureRunning, {
@@ -12914,6 +13050,12 @@ async function connectWithRetry({
12914
13050
  OtelTracing.setActiveSpanAttributes({
12915
13051
  ["factory.daemon.connect_attempts" /* FACTORY_DAEMON_CONNECT_ATTEMPTS */]: attempts
12916
13052
  });
13053
+ if (attempts > 0) {
13054
+ try {
13055
+ onAttempts?.(attempts);
13056
+ } catch {
13057
+ }
13058
+ }
12917
13059
  }
12918
13060
  }
12919
13061
 
@@ -14295,7 +14437,7 @@ function createWebSocketDaemonClient(config) {
14295
14437
  }
14296
14438
 
14297
14439
  // ../daemon-sdk/src/session/DaemonSessionController.ts
14298
- import { EventEmitter as EventEmitter6 } from "eventemitter3";
14440
+ import { EventEmitter as EventEmitter4 } from "eventemitter3";
14299
14441
  import { v4 as uuidv47 } from "uuid";
14300
14442
 
14301
14443
  // ../daemon-sdk/src/session/connection-status.ts
@@ -14376,7 +14518,7 @@ var MessageRouter = class _MessageRouter extends EventEmitter3 {
14376
14518
  requestId: msg.id,
14377
14519
  sessionId: msg.params.sessionId,
14378
14520
  method: msg.method,
14379
- toolCount: msg.params.toolUses.length
14521
+ count: msg.params.toolUses.length
14380
14522
  });
14381
14523
  this.emit("permissionRequest", {
14382
14524
  request: msg,
@@ -14403,7 +14545,7 @@ var MessageRouter = class _MessageRouter extends EventEmitter3 {
14403
14545
  requestId: msg.id,
14404
14546
  sessionId: msg.params.sessionId,
14405
14547
  method: msg.method,
14406
- questionCount: msg.params.questions.length,
14548
+ count: msg.params.questions.length,
14407
14549
  toolCallId: msg.params.toolCallId
14408
14550
  });
14409
14551
  this.emit("askUserRequest", {
@@ -14515,7 +14657,7 @@ var MessageRouter = class _MessageRouter extends EventEmitter3 {
14515
14657
  "type"
14516
14658
  ]);
14517
14659
  logInfo("[MessageRouter] Received unsupported session notification", {
14518
- notificationType: notificationType ?? void 0,
14660
+ type: notificationType ?? void 0,
14519
14661
  isError: !!protocolVersionMismatch,
14520
14662
  version: protocolVersionMismatch?.localFactoryProtocolVersion,
14521
14663
  sessionId: sessionId ?? void 0
@@ -14637,507 +14779,148 @@ var ReconnectionStrategy = class {
14637
14779
  }
14638
14780
  };
14639
14781
 
14640
- // ../daemon-sdk/src/session/handlers/AskUserRequestHandler.ts
14641
- import { EventEmitter as EventEmitter4 } from "eventemitter3";
14642
- var AskUserRequestHandler = class _AskUserRequestHandler extends EventEmitter4 {
14643
- pendingRequests = /* @__PURE__ */ new Map();
14644
- handleAskUserRequest(request, id, sessionId, context) {
14645
- logInfo("[AskUserRequestHandler] Handling ask-user request", {
14646
- requestId: id,
14647
- sessionId,
14648
- toolCallId: request.params.toolCallId,
14649
- questionCount: request.params.questions.length
14650
- });
14651
- const pending = {
14652
- requestId: id,
14653
- sessionId,
14654
- toolCallId: request.params.toolCallId,
14655
- questions: request.params.questions,
14656
- timestamp: Date.now(),
14657
- ...context?.isRestored ? { isRestored: true } : {}
14658
- };
14659
- const existingRequest = this.pendingRequests.get(id);
14660
- if (existingRequest) {
14661
- this.pendingRequests.set(id, pending);
14662
- logInfo("[AskUserRequestHandler] Duplicate ask-user request replay", {
14663
- requestId: id,
14664
- sessionId
14665
- });
14666
- return;
14667
- }
14668
- this.pendingRequests.set(id, pending);
14669
- this.emit("askUserRequested", pending);
14670
- }
14671
- resolveAskUser(id, result) {
14672
- const pending = this.pendingRequests.get(id);
14673
- if (!pending) {
14674
- logWarn("[AskUserRequestHandler] No pending ask-user request found", {
14675
- requestId: id
14676
- });
14677
- return;
14678
- }
14679
- _AskUserRequestHandler.validateResult(pending, result);
14680
- this.pendingRequests.delete(id);
14681
- this.emit("askUserResolved", id, result);
14682
- }
14683
- clearAskUser(id) {
14684
- const pending = this.pendingRequests.get(id);
14685
- if (!pending) {
14686
- logWarn(
14687
- "[AskUserRequestHandler] No pending ask-user request found to clear",
14688
- {
14689
- requestId: id
14690
- }
14691
- );
14692
- return;
14693
- }
14694
- this.pendingRequests.delete(id);
14695
- this.emit("askUserResolved", id);
14782
+ // ../daemon-sdk/src/session/state/enums.ts
14783
+ var QueuedUserMessageKind = /* @__PURE__ */ ((QueuedUserMessageKind2) => {
14784
+ QueuedUserMessageKind2["LocalDeferredAfterEsc"] = "local_deferred_after_esc";
14785
+ QueuedUserMessageKind2["LocalPausedAfterEsc"] = "local_paused_after_esc";
14786
+ QueuedUserMessageKind2["DaemonQueuedDiscardable"] = "daemon_queued_discardable";
14787
+ QueuedUserMessageKind2["DaemonQueuedEndOfLoop"] = "daemon_queued_end_of_loop";
14788
+ QueuedUserMessageKind2["LocalDeferredDuringManualCompaction"] = "local_deferred_during_manual_compaction";
14789
+ return QueuedUserMessageKind2;
14790
+ })(QueuedUserMessageKind || {});
14791
+ var QueuedUserMessageDisplayGroup = /* @__PURE__ */ ((QueuedUserMessageDisplayGroup2) => {
14792
+ QueuedUserMessageDisplayGroup2["Steering"] = "steering";
14793
+ QueuedUserMessageDisplayGroup2["Queued"] = "queued";
14794
+ return QueuedUserMessageDisplayGroup2;
14795
+ })(QueuedUserMessageDisplayGroup || {});
14796
+
14797
+ // ../daemon-sdk/src/session/state/queuedUserMessageHelpers.ts
14798
+ var QUEUED_MESSAGE_KIND_METADATA = {
14799
+ ["local_deferred_after_esc" /* LocalDeferredAfterEsc */]: {
14800
+ displayGroup: "steering" /* Steering */
14801
+ },
14802
+ ["local_paused_after_esc" /* LocalPausedAfterEsc */]: {
14803
+ reviewable: true,
14804
+ displayGroup: "queued" /* Queued */,
14805
+ reviewPriority: 1
14806
+ },
14807
+ ["daemon_queued_discardable" /* DaemonQueuedDiscardable */]: {
14808
+ daemonBacked: true,
14809
+ reviewable: true,
14810
+ displayGroup: "steering" /* Steering */,
14811
+ reviewPriority: 0
14812
+ },
14813
+ ["daemon_queued_end_of_loop" /* DaemonQueuedEndOfLoop */]: {
14814
+ daemonBacked: true,
14815
+ reviewable: true,
14816
+ displayGroup: "queued" /* Queued */,
14817
+ reviewPriority: 1
14818
+ },
14819
+ ["local_deferred_during_manual_compaction" /* LocalDeferredDuringManualCompaction */]: {
14820
+ displayGroup: "queued" /* Queued */
14696
14821
  }
14697
- clearSessionAskUserRequests(sessionId) {
14698
- const requestsToRemove = Array.from(this.pendingRequests.values()).filter(
14699
- (request) => request.sessionId === sessionId
14700
- );
14701
- for (const request of requestsToRemove) {
14702
- this.clearAskUser(request.requestId);
14822
+ };
14823
+ function isDaemonQueuedMessageKind(kind) {
14824
+ return QUEUED_MESSAGE_KIND_METADATA[kind].daemonBacked === true;
14825
+ }
14826
+ function isReviewableQueuedMessageKind(kind) {
14827
+ return QUEUED_MESSAGE_KIND_METADATA[kind].reviewable === true;
14828
+ }
14829
+ function getQueuedUserMessageDisplayGroup(kind) {
14830
+ return QUEUED_MESSAGE_KIND_METADATA[kind].displayGroup;
14831
+ }
14832
+ function getQueuedUserMessageReviewPriority(kind) {
14833
+ return QUEUED_MESSAGE_KIND_METADATA[kind].reviewPriority ?? null;
14834
+ }
14835
+ function getQueuedUserMessageKindForQueuePlacement(queuePlacement) {
14836
+ return queuePlacement === "end_of_loop" /* EndOfLoop */ ? "daemon_queued_end_of_loop" /* DaemonQueuedEndOfLoop */ : "daemon_queued_discardable" /* DaemonQueuedDiscardable */;
14837
+ }
14838
+
14839
+ // ../daemon-sdk/src/session/queueLoadReconciliation.ts
14840
+ function getSendableContentFromBlocks(content) {
14841
+ const images = [];
14842
+ const files = [];
14843
+ const textBlocks = [];
14844
+ const orderedContent = [];
14845
+ for (const block of content) {
14846
+ if (block.type === "image" /* Image */) {
14847
+ images.push(block.source);
14848
+ orderedContent.push(block);
14849
+ } else if (block.type === "document" /* Document */) {
14850
+ files.push(block.source);
14851
+ } else if (block.type === "text" /* Text */) {
14852
+ textBlocks.push(block.text);
14853
+ orderedContent.push(block);
14703
14854
  }
14704
14855
  }
14705
- cancelAskUser(id) {
14706
- this.resolveAskUser(id, { cancelled: true, answers: [] });
14707
- }
14708
- getPendingAskUserRequests() {
14709
- return Array.from(this.pendingRequests.values());
14710
- }
14711
- getPendingAskUserRequest(id) {
14712
- return this.pendingRequests.get(id);
14856
+ return {
14857
+ text: textBlocks.join("\n").trim(),
14858
+ images,
14859
+ files,
14860
+ orderedContent
14861
+ };
14862
+ }
14863
+ function getQueuePlacementForQueuedMessageKind(kind) {
14864
+ return kind === "daemon_queued_end_of_loop" /* DaemonQueuedEndOfLoop */ ? "end_of_loop" /* EndOfLoop */ : "end_of_turn" /* EndOfTurn */;
14865
+ }
14866
+ function documentSourcesMatch(left, right) {
14867
+ if (left.type !== right.type || left.mediaType !== right.mediaType) {
14868
+ return false;
14713
14869
  }
14714
- clearAll() {
14715
- this.pendingRequests.clear();
14870
+ if (left.type === "base64" /* Base64 */ && right.type === "base64" /* Base64 */) {
14871
+ return left.data === right.data && left.name === right.name && left.parsedData === right.parsedData && left.path === right.path;
14716
14872
  }
14717
- destroy() {
14718
- this.clearAll();
14719
- this.removeAllListeners();
14873
+ if (left.type === "text" /* Text */ && right.type === "text" /* Text */) {
14874
+ return left.data === right.data && left.name === right.name && left.mime === right.mime;
14720
14875
  }
14721
- static validateResult(pending, result) {
14722
- if (result.cancelled === true) {
14723
- return;
14876
+ return false;
14877
+ }
14878
+ function imageSourcesMatch(left, right) {
14879
+ return left.type === right.type && left.data === right.data && left.mediaType === right.mediaType;
14880
+ }
14881
+ function arrayItemsMatch(left, right, matches) {
14882
+ return left.length === right.length && left.every((leftItem, index) => matches(leftItem, right[index]));
14883
+ }
14884
+ function orderedContentBlocksMatch(left, right) {
14885
+ return arrayItemsMatch(left, right, (leftBlock, rightBlock) => {
14886
+ if (leftBlock.type !== rightBlock.type) return false;
14887
+ if (leftBlock.type === "text" /* Text */ && rightBlock.type === "text" /* Text */) {
14888
+ return leftBlock.text === rightBlock.text;
14724
14889
  }
14725
- const questionsByIndex = new Map(
14726
- pending.questions.map((q) => [q.index, q])
14727
- );
14728
- const answered = /* @__PURE__ */ new Set();
14729
- for (const answer of result.answers) {
14730
- const question = questionsByIndex.get(answer.index);
14731
- if (!question) {
14732
- throw new MetaError("Answer references unknown question index", {
14733
- requestId: pending.requestId,
14734
- index: answer.index
14735
- });
14736
- }
14737
- if (answered.has(answer.index)) {
14738
- throw new MetaError("Duplicate answers for question index", {
14739
- requestId: pending.requestId,
14740
- index: answer.index
14741
- });
14742
- }
14743
- answered.add(answer.index);
14744
- }
14745
- const missing = pending.questions.map((q) => q.index).filter((idx) => !answered.has(idx));
14746
- if (missing.length > 0) {
14747
- throw new MetaError("Missing answers for some questions", {
14748
- requestId: pending.requestId,
14749
- missing: missing.map(String)
14750
- });
14751
- }
14752
- }
14753
- };
14754
-
14755
- // ../daemon-sdk/src/session/handlers/PendingUserActionStore.ts
14756
- var PendingUserActionStore = class {
14757
- permissionsByToolUseId = /* @__PURE__ */ new Map();
14758
- askUsersBySession = /* @__PURE__ */ new Map();
14759
- savePermission(action) {
14760
- this.permissionsByToolUseId.set(action.toolUseId, action);
14761
- logInfo("[PendingUserActionStore] Stored permission for later replay", {
14762
- sessionId: action.sessionId,
14763
- requestId: action.requestId,
14764
- toolUseId: action.toolUseId,
14765
- selectedOptionLabel: action.selectedOption
14766
- });
14767
- }
14768
- saveAskUser(action) {
14769
- let map = this.askUsersBySession.get(action.sessionId);
14770
- if (!map) {
14771
- map = /* @__PURE__ */ new Map();
14772
- this.askUsersBySession.set(action.sessionId, map);
14773
- }
14774
- map.set(action.toolCallId, action);
14775
- logInfo("[PendingUserActionStore] Stored ask-user for later replay", {
14776
- sessionId: action.sessionId,
14777
- requestId: action.requestId,
14778
- toolCallId: action.toolCallId
14779
- });
14780
- }
14781
- /** Peek a buffered permission response by its globally-unique tool-use id. */
14782
- getForToolUse(toolUseId) {
14783
- return this.permissionsByToolUseId.get(toolUseId);
14784
- }
14785
- getForToolCall(sessionId, toolCallId) {
14786
- return this.askUsersBySession.get(sessionId)?.get(toolCallId);
14787
- }
14788
- /** Remove and return a buffered permission response by tool-use id. */
14789
- takeForToolUse(toolUseId) {
14790
- const action = this.permissionsByToolUseId.get(toolUseId);
14791
- if (!action) return void 0;
14792
- this.permissionsByToolUseId.delete(toolUseId);
14793
- return action;
14794
- }
14795
- takeForToolCall(sessionId, toolCallId) {
14796
- const map = this.askUsersBySession.get(sessionId);
14797
- if (!map) return void 0;
14798
- const action = map.get(toolCallId);
14799
- if (!action) return void 0;
14800
- map.delete(toolCallId);
14801
- if (map.size === 0) this.askUsersBySession.delete(sessionId);
14802
- return action;
14803
- }
14804
- getPermissionsForSession(sessionId) {
14805
- return Array.from(this.permissionsByToolUseId.values()).filter(
14806
- (action) => action.sessionId === sessionId
14807
- );
14808
- }
14809
- getAskUsersForSession(sessionId) {
14810
- const map = this.askUsersBySession.get(sessionId);
14811
- return map ? Array.from(map.values()) : [];
14812
- }
14813
- clearSession(sessionId) {
14814
- let permCount = 0;
14815
- for (const [toolUseId, action] of this.permissionsByToolUseId) {
14816
- if (action.sessionId === sessionId) {
14817
- this.permissionsByToolUseId.delete(toolUseId);
14818
- permCount += 1;
14819
- }
14820
- }
14821
- const askCount = this.askUsersBySession.get(sessionId)?.size ?? 0;
14822
- this.askUsersBySession.delete(sessionId);
14823
- const total = permCount + askCount;
14824
- if (total > 0) {
14825
- logInfo("[PendingUserActionStore] Cleared session", {
14826
- sessionId,
14827
- pendingRequestCount: permCount,
14828
- questionCount: askCount
14829
- });
14890
+ if (leftBlock.type === "image" /* Image */ && rightBlock.type === "image" /* Image */) {
14891
+ return imageSourcesMatch(leftBlock.source, rightBlock.source) && leftBlock.generated === rightBlock.generated;
14830
14892
  }
14831
- return total;
14832
- }
14833
- clearAll() {
14834
- this.permissionsByToolUseId.clear();
14835
- this.askUsersBySession.clear();
14836
- }
14837
- destroy() {
14838
- this.clearAll();
14839
- }
14840
- };
14841
-
14842
- // ../daemon-sdk/src/session/handlers/PermissionRequestHandler.ts
14843
- import { EventEmitter as EventEmitter5 } from "eventemitter3";
14844
- function normalizeAssociatedSessionIds(sessionId, associatedSessionIds) {
14845
- return Array.from(/* @__PURE__ */ new Set([sessionId, ...associatedSessionIds ?? []]));
14893
+ return false;
14894
+ });
14846
14895
  }
14847
- var PermissionRequestHandler = class extends EventEmitter5 {
14848
- pendingPermissions = /* @__PURE__ */ new Map();
14849
- statistics = {
14850
- total: 0,
14851
- approved: 0,
14852
- denied: 0,
14853
- timedOut: 0
14854
- };
14855
- /**
14856
- * Handle an incoming permission request
14857
- */
14858
- handlePermissionRequest(request, id, sessionId, context) {
14859
- logInfo("[PermissionRequestHandler] Handling permission request", {
14860
- requestId: id,
14861
- sessionId,
14862
- toolCount: request.params.toolUses.length,
14863
- options: request.params.options
14864
- });
14865
- const { options } = request.params;
14866
- const toolUses = request.params.toolUses.map((tool) => ({
14867
- ...tool,
14868
- toolUse: {
14869
- ...tool.toolUse,
14870
- input: getPermissionToolInputForDisplay2({
14871
- confirmationType: tool.confirmationType,
14872
- details: tool.details,
14873
- toolInput: tool.toolUse.input
14874
- })
14875
- }
14876
- }));
14877
- const requestAssociatedSessionIds = normalizeAssociatedSessionIds(
14878
- sessionId,
14879
- request.params.associatedSessionIds
14880
- );
14881
- const existingPermission = this.pendingPermissions.get(id);
14882
- const associatedSessionIds = existingPermission ? normalizeAssociatedSessionIds(sessionId, [
14883
- ...existingPermission.associatedSessionIds,
14884
- ...requestAssociatedSessionIds
14885
- ]) : requestAssociatedSessionIds;
14886
- const pendingPermission = {
14887
- requestId: id,
14888
- sessionId,
14889
- associatedSessionIds,
14890
- toolUses,
14891
- options,
14892
- timestamp: Date.now(),
14893
- ...context?.isRestored ? { isRestored: true } : {}
14894
- };
14895
- if (existingPermission) {
14896
- this.pendingPermissions.set(id, pendingPermission);
14897
- const addedSurface = associatedSessionIds.some(
14898
- (associatedSessionId) => !existingPermission.associatedSessionIds.includes(associatedSessionId)
14899
- );
14900
- logInfo(
14901
- "[PermissionRequestHandler] Duplicate permission request replay",
14902
- {
14903
- requestId: id,
14904
- sessionId
14905
- }
14906
- );
14907
- if (addedSurface) {
14908
- this.emit("permissionRequested", pendingPermission);
14909
- }
14910
- return;
14911
- }
14912
- const incomingToolUseIds = new Set(
14913
- toolUses.map((toolUse) => toolUse.toolUse.id)
14914
- );
14915
- for (const [pendingId, pending] of this.pendingPermissions.entries()) {
14916
- if (pending.toolUses.some(
14917
- (toolUse) => incomingToolUseIds.has(toolUse.toolUse.id)
14918
- )) {
14919
- this.clearPermission(pendingId, { emitResolved: true });
14920
- }
14921
- }
14922
- this.pendingPermissions.set(id, pendingPermission);
14923
- this.statistics.total++;
14924
- logInfo("[PermissionRequestHandler] Stored pending permission", {
14925
- requestId: id,
14926
- totalCount: this.pendingPermissions.size
14927
- });
14928
- logInfo("[PermissionRequestHandler] Emitting permissionRequested event", {
14929
- requestId: id
14930
- });
14931
- this.emit("permissionRequested", pendingPermission);
14932
- }
14933
- /**
14934
- * Resolve a permission request with user's choice
14935
- */
14936
- resolvePermission(id, option) {
14937
- const pending = this.pendingPermissions.get(id);
14938
- if (!pending) {
14939
- throw new MetaError("No pending permission found", { requestId: id });
14940
- }
14941
- const isValidOption = pending.options.some((opt) => opt.value === option);
14942
- if (!isValidOption) {
14943
- const allowedOptions = pending.options.map((opt) => opt.value);
14944
- throw new MetaError(
14945
- `Invalid permission option: ${option}. Allowed: ${allowedOptions.join(", ")}`,
14946
- {
14947
- requestId: id
14948
- }
14949
- );
14950
- }
14951
- this.pendingPermissions.delete(id);
14952
- if (option !== "cancel" /* Cancel */) {
14953
- this.statistics.approved++;
14954
- } else {
14955
- this.statistics.denied++;
14956
- }
14957
- this.emit("permissionResolved", id, option);
14958
- }
14959
- /**
14960
- * Clear a specific permission without an outcome.
14961
- */
14962
- clearPermission(id, options) {
14963
- this.pendingPermissions.delete(id);
14964
- if (options?.emitResolved) {
14965
- this.emit("permissionResolved", id);
14966
- }
14967
- }
14968
- /**
14969
- * Clear all pending permissions for a specific session.
14970
- *
14971
- * With `preserveRelayed`, permissions surfaced on multiple sessions (a
14972
- * relayed subagent permission whose `associatedSessionIds` extend beyond this
14973
- * session) are kept. loadSession uses this so reloading the parent doesn't
14974
- * wipe a child's still-pending prompt that the daemon's parent-session load
14975
- * never returns.
14976
- */
14977
- clearSessionPermissions(sessionId, options) {
14978
- const permissionsToRemove = [];
14979
- for (const [id, permission] of this.pendingPermissions.entries()) {
14980
- if (permission.sessionId !== sessionId) {
14981
- continue;
14982
- }
14983
- if (options?.preserveRelayed && permission.associatedSessionIds.some(
14984
- (associatedId) => associatedId !== sessionId
14985
- )) {
14986
- continue;
14987
- }
14988
- permissionsToRemove.push(id);
14989
- }
14990
- for (const id of permissionsToRemove) {
14991
- this.clearPermission(id, options);
14992
- }
14993
- logInfo("[PermissionRequestHandler] Cleared permissions for session", {
14994
- sessionId,
14995
- count: permissionsToRemove.length
14896
+ function hasCanonicalFallbackOrder(content) {
14897
+ const fallbackContent = [
14898
+ ...content.images.map(
14899
+ (source) => ({
14900
+ type: "image" /* Image */,
14901
+ source
14902
+ })
14903
+ )
14904
+ ];
14905
+ if (content.text) {
14906
+ fallbackContent.push({
14907
+ type: "text" /* Text */,
14908
+ text: content.text
14996
14909
  });
14997
14910
  }
14998
- /**
14999
- * Clear all pending permissions
15000
- */
15001
- clearAllPermissions() {
15002
- this.pendingPermissions.clear();
15003
- }
15004
- /**
15005
- * Get all pending permissions
15006
- */
15007
- getPendingPermissions() {
15008
- return Array.from(this.pendingPermissions.values());
15009
- }
15010
- getPendingPermissionsForSession(sessionId) {
15011
- return this.getPendingPermissions().filter(
15012
- (permission) => permission.associatedSessionIds.includes(sessionId)
15013
- );
15014
- }
15015
- /**
15016
- * Get a specific pending permission
15017
- */
15018
- getPendingPermission(id) {
15019
- return this.pendingPermissions.get(id);
15020
- }
15021
- /**
15022
- * Check if there are any pending permissions
15023
- */
15024
- hasPendingPermissions() {
15025
- return this.pendingPermissions.size > 0;
15026
- }
15027
- /**
15028
- * Destroy and clean up
15029
- */
15030
- destroy() {
15031
- this.clearAllPermissions();
15032
- this.removeAllListeners();
15033
- }
15034
- };
15035
-
15036
- // ../daemon-sdk/src/session/state/enums.ts
15037
- var QueuedUserMessageKind = /* @__PURE__ */ ((QueuedUserMessageKind2) => {
15038
- QueuedUserMessageKind2["LocalDeferredAfterEsc"] = "local_deferred_after_esc";
15039
- QueuedUserMessageKind2["LocalPausedAfterEsc"] = "local_paused_after_esc";
15040
- QueuedUserMessageKind2["DaemonQueuedDiscardable"] = "daemon_queued_discardable";
15041
- QueuedUserMessageKind2["DaemonQueuedEndOfLoop"] = "daemon_queued_end_of_loop";
15042
- QueuedUserMessageKind2["LocalDeferredDuringManualCompaction"] = "local_deferred_during_manual_compaction";
15043
- return QueuedUserMessageKind2;
15044
- })(QueuedUserMessageKind || {});
15045
- var QueuedUserMessageDisplayGroup = /* @__PURE__ */ ((QueuedUserMessageDisplayGroup2) => {
15046
- QueuedUserMessageDisplayGroup2["Steering"] = "steering";
15047
- QueuedUserMessageDisplayGroup2["Queued"] = "queued";
15048
- return QueuedUserMessageDisplayGroup2;
15049
- })(QueuedUserMessageDisplayGroup || {});
15050
-
15051
- // ../daemon-sdk/src/session/state/queuedUserMessageHelpers.ts
15052
- var QUEUED_MESSAGE_KIND_METADATA = {
15053
- ["local_deferred_after_esc" /* LocalDeferredAfterEsc */]: {
15054
- displayGroup: "steering" /* Steering */
15055
- },
15056
- ["local_paused_after_esc" /* LocalPausedAfterEsc */]: {
15057
- reviewable: true,
15058
- displayGroup: "queued" /* Queued */,
15059
- reviewPriority: 1
15060
- },
15061
- ["daemon_queued_discardable" /* DaemonQueuedDiscardable */]: {
15062
- daemonBacked: true,
15063
- reviewable: true,
15064
- displayGroup: "steering" /* Steering */,
15065
- reviewPriority: 0
15066
- },
15067
- ["daemon_queued_end_of_loop" /* DaemonQueuedEndOfLoop */]: {
15068
- daemonBacked: true,
15069
- reviewable: true,
15070
- displayGroup: "queued" /* Queued */,
15071
- reviewPriority: 1
15072
- },
15073
- ["local_deferred_during_manual_compaction" /* LocalDeferredDuringManualCompaction */]: {
15074
- displayGroup: "queued" /* Queued */
15075
- }
15076
- };
15077
- function isDaemonQueuedMessageKind(kind) {
15078
- return QUEUED_MESSAGE_KIND_METADATA[kind].daemonBacked === true;
15079
- }
15080
- function isReviewableQueuedMessageKind(kind) {
15081
- return QUEUED_MESSAGE_KIND_METADATA[kind].reviewable === true;
15082
- }
15083
- function getQueuedUserMessageDisplayGroup(kind) {
15084
- return QUEUED_MESSAGE_KIND_METADATA[kind].displayGroup;
15085
- }
15086
- function getQueuedUserMessageReviewPriority(kind) {
15087
- return QUEUED_MESSAGE_KIND_METADATA[kind].reviewPriority ?? null;
15088
- }
15089
- function getQueuedUserMessageKindForQueuePlacement(queuePlacement) {
15090
- return queuePlacement === "end_of_loop" /* EndOfLoop */ ? "daemon_queued_end_of_loop" /* DaemonQueuedEndOfLoop */ : "daemon_queued_discardable" /* DaemonQueuedDiscardable */;
15091
- }
15092
-
15093
- // ../daemon-sdk/src/session/queueLoadReconciliation.ts
15094
- function getSendableContentFromBlocks(content) {
15095
- const images = [];
15096
- const files = [];
15097
- const textBlocks = [];
15098
- for (const block of content) {
15099
- if (block.type === "image" /* Image */) {
15100
- images.push(block.source);
15101
- } else if (block.type === "document" /* Document */) {
15102
- files.push(block.source);
15103
- } else if (block.type === "text" /* Text */) {
15104
- textBlocks.push(block.text);
15105
- }
15106
- }
15107
- return {
15108
- text: textBlocks.join("\n").trim(),
15109
- images,
15110
- files
15111
- };
15112
- }
15113
- function getQueuePlacementForQueuedMessageKind(kind) {
15114
- return kind === "daemon_queued_end_of_loop" /* DaemonQueuedEndOfLoop */ ? "end_of_loop" /* EndOfLoop */ : "end_of_turn" /* EndOfTurn */;
15115
- }
15116
- function documentSourcesMatch(left, right) {
15117
- if (left.type !== right.type || left.mediaType !== right.mediaType) {
15118
- return false;
15119
- }
15120
- if (left.type === "base64" /* Base64 */ && right.type === "base64" /* Base64 */) {
15121
- return left.data === right.data && left.name === right.name && left.parsedData === right.parsedData && left.path === right.path;
15122
- }
15123
- if (left.type === "text" /* Text */ && right.type === "text" /* Text */) {
15124
- return left.data === right.data && left.name === right.name && left.mime === right.mime;
15125
- }
15126
- return false;
15127
- }
15128
- function imageSourcesMatch(left, right) {
15129
- return left.type === right.type && left.data === right.data && left.mediaType === right.mediaType;
15130
- }
15131
- function arrayItemsMatch(left, right, matches) {
15132
- return left.length === right.length && left.every((leftItem, index) => matches(leftItem, right[index]));
14911
+ return orderedContentBlocksMatch(content.orderedContent, fallbackContent);
15133
14912
  }
15134
14913
  function sendableContentMatches(left, right) {
15135
- return left.text === right.text && arrayItemsMatch(left.images, right.images, imageSourcesMatch) && arrayItemsMatch(left.files, right.files, documentSourcesMatch);
14914
+ const fallbackFieldsMatch = left.text === right.text && arrayItemsMatch(left.images, right.images, imageSourcesMatch) && arrayItemsMatch(left.files, right.files, documentSourcesMatch);
14915
+ if (!fallbackFieldsMatch) return false;
14916
+ if (left.files.length > 0 || right.files.length > 0) return true;
14917
+ return hasCanonicalFallbackOrder(left) && hasCanonicalFallbackOrder(right) || orderedContentBlocksMatch(left.orderedContent, right.orderedContent);
15136
14918
  }
15137
14919
  function buildStaleQueuedMessageResubmitParams(message) {
15138
14920
  const content = getSendableContentFromBlocks(message.content);
15139
14921
  return {
15140
14922
  text: content.text,
14923
+ ...content.files.length === 0 && !hasCanonicalFallbackOrder(content) ? { content: content.orderedContent } : {},
15141
14924
  ...content.images.length > 0 ? { images: content.images } : {},
15142
14925
  ...content.files.length > 0 ? { files: content.files } : {},
15143
14926
  queuePlacement: getQueuePlacementForQueuedMessageKind(message.kind)
@@ -15176,6 +14959,7 @@ async function reconcileQueuedMessagesAfterLoad(params) {
15176
14959
  (queuedMsg) => {
15177
14960
  const content = buildUserMessageContentBlocks2({
15178
14961
  text: queuedMsg.text,
14962
+ content: queuedMsg.content,
15179
14963
  images: queuedMsg.images,
15180
14964
  files: queuedMsg.files
15181
14965
  });
@@ -15320,29 +15104,42 @@ function taskInvocationStatusForCompletionReason(reason) {
15320
15104
  return "failed" /* Failed */;
15321
15105
  }
15322
15106
  }
15323
- var DaemonSessionController = class _DaemonSessionController extends EventEmitter6 {
15107
+ var DaemonSessionController = class _DaemonSessionController extends EventEmitter4 {
15324
15108
  daemonClient;
15325
15109
  messageRouter;
15326
- permissionHandler;
15110
+ // Session-scoped prompt state (pending permissions, pending ask-user
15111
+ // requests, buffered answers) lives on the shared app-lifetime
15112
+ // MultiSessionStateManager, not on this per-connection controller, so it
15113
+ // survives controller teardown/recreation. These accessors keep call sites
15114
+ // unchanged.
15115
+ get permissionHandler() {
15116
+ return this.sessionStateManager.permissionRequests;
15117
+ }
15327
15118
  // Per-session permission subscribers. A subagent (child Task) permission is
15328
15119
  // fanned out to every subscribed session in its `associatedSessionIds`, so a
15329
15120
  // child's prompt surfaces on both the child and its parent.
15330
15121
  sessionPermissionSubscribers = /* @__PURE__ */ new Map();
15331
- askUserHandler;
15122
+ get askUserHandler() {
15123
+ return this.sessionStateManager.askUserRequests;
15124
+ }
15332
15125
  // Buffered prompt responses for inactive sessions.
15333
- pendingUserActionStore;
15334
- // Disconnected sessions whose prompt responses should be buffered.
15335
- inactiveSessions = /* @__PURE__ */ new Set();
15336
- // Sessions currently draining a batch of concurrent prompts (e.g. parallel
15337
- // foreground subagents each relaying an Execute permission to the parent
15338
- // session). While draining, a resume reflects one prompt being answered while
15339
- // siblings remain genuinely pending, so resume-driven auto-clear is
15340
- // suppressed until every prompt for the session has resolved.
15341
- sessionsWithConcurrentPrompts = /* @__PURE__ */ new Set();
15342
- // Sessions with a self-resume loadSession already in flight.
15126
+ get pendingUserActionStore() {
15127
+ return this.sessionStateManager.pendingUserActions;
15128
+ }
15129
+ // Teardown hooks for this controller's listeners on the shared prompt
15130
+ // stores. The stores outlive the controller, so destroy() must detach
15131
+ // exactly this controller's listeners (removeAllListeners would detach
15132
+ // other controllers' too).
15133
+ promptStoreUnsubscribes = [];
15134
+ // Whether a session's worker is paused (buffer responses), whether a session
15135
+ // is draining a concurrent prompt batch (suppress resume-driven auto-clear),
15136
+ // and the spawn options replayed on self-resume all describe the *session*,
15137
+ // not this connection, so they live on the shared app-lifetime
15138
+ // MultiSessionStateManager and survive controller teardown/recreation.
15139
+ // Sessions with a self-resume loadSession already in flight. Stays on this
15140
+ // controller: it dedupes an in-flight RPC/retry loop on this connection's
15141
+ // daemonClient, not session state.
15343
15142
  selfResumeInFlight = /* @__PURE__ */ new Set();
15344
- // Last loadSession spawn options, reused for self-resume.
15345
- sessionLoadOptions = /* @__PURE__ */ new Map();
15346
15143
  // addUserMessage responses can resolve after their CREATE_MESSAGE notification.
15347
15144
  // Track that ordering so an already-confirmed turn is not added to the
15348
15145
  // visible queue after its real user message has been rendered.
@@ -15400,9 +15197,6 @@ var DaemonSessionController = class _DaemonSessionController extends EventEmitte
15400
15197
  this.messageRouter = new MessageRouter({
15401
15198
  emitReceiveSpans: config.clientType !== "cli"
15402
15199
  });
15403
- this.permissionHandler = new PermissionRequestHandler();
15404
- this.askUserHandler = new AskUserRequestHandler();
15405
- this.pendingUserActionStore = new PendingUserActionStore();
15406
15200
  this.sessionStateManager = sessionStateManager;
15407
15201
  this.missionStateManager = missionStateManager ?? null;
15408
15202
  const reconnectDelegated = config.machineType === "local" /* Local */ ? config.reconnectDelegated ?? false : false;
@@ -15453,12 +15247,17 @@ var DaemonSessionController = class _DaemonSessionController extends EventEmitte
15453
15247
  this.connectAttemptContext = previous;
15454
15248
  }
15455
15249
  }
15456
- /**
15457
- * Check if the client is a desktop app.
15458
- * Uses injectable clientType from config instead of checking window.electronAPI.
15459
- */
15460
- isDesktopApp() {
15461
- return this.getClientType() === "desktop";
15250
+ getFactoryClientType() {
15251
+ switch (this.getClientType()) {
15252
+ case "desktop":
15253
+ return "web-desktop" /* WebDesktop */;
15254
+ case "cli":
15255
+ return "cli" /* CLI */;
15256
+ case "sdk":
15257
+ return "sdk" /* SDK */;
15258
+ case "web":
15259
+ return "web-app" /* WebApp */;
15260
+ }
15462
15261
  }
15463
15262
  /**
15464
15263
  * Get the mission store for a session.
@@ -15763,9 +15562,10 @@ var DaemonSessionController = class _DaemonSessionController extends EventEmitte
15763
15562
  }
15764
15563
  case "droid_working_state_changed" /* DROID_WORKING_STATE_CHANGED */: {
15765
15564
  const { type: _, ...stateNotification } = notification;
15565
+ const effectiveState = this.sessionStateManager.getSessionManager(sessionId)?.getDroidWorkingState() ?? stateNotification.newState;
15766
15566
  this.emit("droidWorkingStateChanged" /* DroidWorkingStateChanged */, {
15767
15567
  sessionId,
15768
- newState: stateNotification.newState
15568
+ newState: effectiveState
15769
15569
  });
15770
15570
  break;
15771
15571
  }
@@ -16016,15 +15816,15 @@ var DaemonSessionController = class _DaemonSessionController extends EventEmitte
16016
15816
  }
16017
15817
  logInfo("[DaemonSessionController] Clearing pending user actions", {
16018
15818
  sessionId,
16019
- pendingRequestCount: pendingPermissionCount,
16020
- questionCount: pendingAskUserCount
15819
+ pendingCount: pendingPermissionCount,
15820
+ totalQuestions: pendingAskUserCount
16021
15821
  });
16022
15822
  this.permissionHandler.clearSessionPermissions(sessionId, {
16023
15823
  emitResolved: true,
16024
15824
  preserveRelayed: options?.preserveRelayedPermissions
16025
15825
  });
16026
15826
  this.askUserHandler.clearSessionAskUserRequests(sessionId);
16027
- this.sessionsWithConcurrentPrompts.delete(sessionId);
15827
+ this.sessionStateManager.clearConcurrentPrompts(sessionId);
16028
15828
  }
16029
15829
  getPendingPermissionCountForSession(sessionId) {
16030
15830
  return this.permissionHandler.getPendingPermissions().filter(
@@ -16034,6 +15834,40 @@ var DaemonSessionController = class _DaemonSessionController extends EventEmitte
16034
15834
  hasPendingUserActionForSession(sessionId) {
16035
15835
  return this.getPendingPermissionCountForSession(sessionId) > 0 || this.askUserHandler.getPendingAskUserRequests().some((request) => request.sessionId === sessionId);
16036
15836
  }
15837
+ /**
15838
+ * A dead worker can be idle while its session is still waiting on a prompt
15839
+ * owned by this controller. Keep the composite session state waiting until
15840
+ * the buffered response is replayed or the prompt is explicitly cleared.
15841
+ */
15842
+ preserveInactivePendingUserActionState(sessionId) {
15843
+ if (!this.sessionStateManager.isSessionInactive(sessionId) || !this.hasPendingUserActionForSession(sessionId)) {
15844
+ return false;
15845
+ }
15846
+ const sessionManager = this.sessionStateManager.getSessionManager(sessionId);
15847
+ if (sessionManager?.getDroidWorkingState() !== "waiting_for_tool_confirmation" /* WaitingForToolConfirmation */) {
15848
+ sessionManager?.setWaitingForConfirmation();
15849
+ }
15850
+ return true;
15851
+ }
15852
+ /**
15853
+ * Rewrite ONLY the inactivity-companion Idle: a paused worker reports Idle
15854
+ * while its session still owes a prompt owned by this controller, and that
15855
+ * bare Idle must not terminalize the composite state. A non-Idle
15856
+ * notification is a genuine wake (a respawned worker or another attached
15857
+ * client) and must pass through untouched — rewriting it would plant a
15858
+ * Waiting state whose revision bump then defeats the load hydration guards
15859
+ * while reconciliation clears the prompt, stranding the session in
15860
+ * WaitingForToolConfirmation with zero answerable prompts.
15861
+ */
15862
+ getEffectiveStateNotification(notification, sessionId) {
15863
+ if (notification.type === "droid_working_state_changed" /* DROID_WORKING_STATE_CHANGED */ && notification.newState === "idle" /* Idle */ && this.sessionStateManager.isSessionInactive(sessionId) && this.hasPendingUserActionForSession(sessionId)) {
15864
+ return {
15865
+ ...notification,
15866
+ newState: "waiting_for_tool_confirmation" /* WaitingForToolConfirmation */
15867
+ };
15868
+ }
15869
+ return notification;
15870
+ }
16037
15871
  markConcurrentPermissionSurfaces(sessionIds) {
16038
15872
  const pendingPermissions = this.permissionHandler.getPendingPermissions();
16039
15873
  for (const sessionId of sessionIds) {
@@ -16045,15 +15879,15 @@ var DaemonSessionController = class _DaemonSessionController extends EventEmitte
16045
15879
  }
16046
15880
  for (const permission of pendingForSession) {
16047
15881
  for (const associatedSessionId of permission.associatedSessionIds) {
16048
- this.sessionsWithConcurrentPrompts.add(associatedSessionId);
15882
+ this.sessionStateManager.markConcurrentPrompts(associatedSessionId);
16049
15883
  }
16050
15884
  }
16051
15885
  }
16052
15886
  }
16053
15887
  clearDrainedConcurrentPromptMarkers(sessionIds) {
16054
15888
  for (const sessionId of sessionIds) {
16055
- if (this.sessionsWithConcurrentPrompts.has(sessionId) && !this.hasPendingUserActionForSession(sessionId)) {
16056
- this.sessionsWithConcurrentPrompts.delete(sessionId);
15889
+ if (this.sessionStateManager.hasConcurrentPrompts(sessionId) && !this.hasPendingUserActionForSession(sessionId)) {
15890
+ this.sessionStateManager.clearConcurrentPrompts(sessionId);
16057
15891
  }
16058
15892
  }
16059
15893
  }
@@ -16071,27 +15905,27 @@ var DaemonSessionController = class _DaemonSessionController extends EventEmitte
16071
15905
  * draining a concurrent batch.
16072
15906
  *
16073
15907
  * An inactive session has NOT resumed — it paused (inactivity timeout /
16074
- * process exit) with its working state reset to Idle. SESSION_INACTIVITY
16075
- * deliberately preserves outstanding prompts "for later replay", so the
16076
- * Idle working-state notification that accompanies the pause must not be
16077
- * mistaken for a resume and clear them. The next successful loadSession
16078
- * reconciles prompts from the daemon (RT-321).
15908
+ * process exit). SESSION_INACTIVITY deliberately preserves outstanding
15909
+ * prompts "for later replay", so a raw Idle worker-state notification that
15910
+ * accompanies the pause must not be mistaken for a resume and clear them.
15911
+ * The controller keeps the effective session state waiting until the next
15912
+ * successful loadSession reconciles prompts from the daemon.
16079
15913
  */
16080
15914
  clearStalePromptsOnResume(sessionId) {
16081
- if (this.inactiveSessions.has(sessionId)) {
15915
+ if (this.sessionStateManager.isSessionInactive(sessionId)) {
16082
15916
  return;
16083
15917
  }
16084
15918
  const pendingPermissionCount = this.getPendingPermissionCountForSession(sessionId);
16085
15919
  const pendingAskUserCount = this.askUserHandler.getPendingAskUserRequests().filter((request) => request.sessionId === sessionId).length;
16086
15920
  if (pendingPermissionCount === 0 && pendingAskUserCount === 0) {
16087
- this.sessionsWithConcurrentPrompts.delete(sessionId);
15921
+ this.sessionStateManager.clearConcurrentPrompts(sessionId);
16088
15922
  return;
16089
15923
  }
16090
15924
  if (pendingPermissionCount > 1 || pendingAskUserCount > 1) {
16091
- this.sessionsWithConcurrentPrompts.add(sessionId);
15925
+ this.sessionStateManager.markConcurrentPrompts(sessionId);
16092
15926
  return;
16093
15927
  }
16094
- if (this.sessionsWithConcurrentPrompts.has(sessionId)) {
15928
+ if (this.sessionStateManager.hasConcurrentPrompts(sessionId)) {
16095
15929
  return;
16096
15930
  }
16097
15931
  this.clearPendingUserActionsForSession(sessionId, {
@@ -16133,30 +15967,31 @@ var DaemonSessionController = class _DaemonSessionController extends EventEmitte
16133
15967
  }
16134
15968
  case "session_inactivity" /* SESSION_INACTIVITY */:
16135
15969
  case "session_process_exited" /* SESSION_PROCESS_EXITED */: {
16136
- this.inactiveSessions.add(sessionId);
15970
+ this.sessionStateManager.markSessionInactive(sessionId);
16137
15971
  const pendingPermissionCount = this.permissionHandler.getPendingPermissions().filter((p) => p.sessionId === sessionId).length;
16138
15972
  const pendingAskUserCount = this.askUserHandler.getPendingAskUserRequests().filter((r) => r.sessionId === sessionId).length;
16139
15973
  logInfo(
16140
15974
  "[DaemonSessionController] Session became inactive; preserving pending prompts for later replay",
16141
15975
  {
16142
15976
  sessionId,
16143
- pendingRequestCount: pendingPermissionCount,
16144
- questionCount: pendingAskUserCount
15977
+ pendingCount: pendingPermissionCount,
15978
+ totalQuestions: pendingAskUserCount
16145
15979
  }
16146
15980
  );
16147
15981
  const sessionManager = this.sessionStateManager.getSessionManager(sessionId);
16148
15982
  if (sessionManager) {
16149
- sessionManager.stopStreaming();
15983
+ const preservesPendingUserAction = this.preserveInactivePendingUserActionState(sessionId);
15984
+ if (!preservesPendingUserAction && sessionManager.getDroidWorkingState() !== "idle" /* Idle */) {
15985
+ sessionManager.applyDroidWorkingState("idle" /* Idle */);
15986
+ }
16150
15987
  this.emit("droidWorkingStateChanged" /* DroidWorkingStateChanged */, {
16151
15988
  sessionId,
16152
- newState: "idle" /* Idle */
15989
+ newState: sessionManager.getDroidWorkingState()
16153
15990
  });
16154
15991
  }
16155
15992
  break;
16156
15993
  }
16157
15994
  case "session_closed" /* SESSION_CLOSED */: {
16158
- this.inactiveSessions.delete(sessionId);
16159
- this.sessionLoadOptions.delete(sessionId);
16160
15995
  this.trackedChildSessionIds.delete(sessionId);
16161
15996
  this.childSessionAttachmentGeneration.delete(sessionId);
16162
15997
  this.clearPendingUserActionsForSession(sessionId);
@@ -16516,10 +16351,6 @@ var DaemonSessionController = class _DaemonSessionController extends EventEmitte
16516
16351
  });
16517
16352
  this.messageRouter.on("notification", ({ notification, sessionId }) => {
16518
16353
  const associatedPermissionSessionIds = notification.type === "permission_resolved" /* PERMISSION_RESOLVED */ ? this.permissionHandler.getPendingPermission(notification.requestId)?.associatedSessionIds.filter((id) => id !== sessionId) ?? [] : [];
16519
- const params = {
16520
- sessionId,
16521
- notification
16522
- };
16523
16354
  this.emit("sessionNotification" /* SessionNotification */, {
16524
16355
  sessionId,
16525
16356
  notification
@@ -16529,7 +16360,19 @@ var DaemonSessionController = class _DaemonSessionController extends EventEmitte
16529
16360
  notification.requestId
16530
16361
  );
16531
16362
  }
16532
- this.sessionStateManager.handleNotification(params);
16363
+ const isInactiveNotification = notification.type === "session_inactivity" /* SESSION_INACTIVITY */ || notification.type === "session_process_exited" /* SESSION_PROCESS_EXITED */;
16364
+ this.sessionStateManager.handleNotification(
16365
+ {
16366
+ sessionId,
16367
+ notification: this.getEffectiveStateNotification(
16368
+ notification,
16369
+ sessionId
16370
+ )
16371
+ },
16372
+ {
16373
+ preserveWorkingStateOnInactive: isInactiveNotification && this.hasPendingUserActionForSession(sessionId)
16374
+ }
16375
+ );
16533
16376
  this.handleNotificationSideEffects(notification, sessionId);
16534
16377
  this.emitDroidEventForNotification(notification, sessionId);
16535
16378
  for (const associatedSessionId of associatedPermissionSessionIds) {
@@ -16573,7 +16416,7 @@ var DaemonSessionController = class _DaemonSessionController extends EventEmitte
16573
16416
  logInfo("[DaemonSessionController] Received permission request", {
16574
16417
  requestId,
16575
16418
  sessionId,
16576
- toolCount: request.params.toolUses.length
16419
+ count: request.params.toolUses.length
16577
16420
  });
16578
16421
  if (!this.sessionStateManager.getSessionManager(sessionId)) {
16579
16422
  logWarn(
@@ -16608,7 +16451,11 @@ var DaemonSessionController = class _DaemonSessionController extends EventEmitte
16608
16451
  this.permissionHandler.handlePermissionRequest(
16609
16452
  requestWithAssociatedSessions,
16610
16453
  requestId,
16611
- sessionId
16454
+ sessionId,
16455
+ // Stamp the owning machine: this controller's daemon sent the
16456
+ // request, so ownership filtering keeps working even when the
16457
+ // session's manager binding is empty or later evicted.
16458
+ { machineId: this.config.machineId }
16612
16459
  );
16613
16460
  this.markConcurrentPermissionSurfaces(
16614
16461
  /* @__PURE__ */ new Set([sessionId, ...associatedSessionIds])
@@ -16622,7 +16469,7 @@ var DaemonSessionController = class _DaemonSessionController extends EventEmitte
16622
16469
  logInfo("[DaemonSessionController] Received ask-user request", {
16623
16470
  requestId,
16624
16471
  sessionId,
16625
- questionCount: request.params.questions.length,
16472
+ count: request.params.questions.length,
16626
16473
  toolCallId: request.params.toolCallId
16627
16474
  });
16628
16475
  if (!this.sessionStateManager.getSessionManager(sessionId)) {
@@ -16638,26 +16485,68 @@ var DaemonSessionController = class _DaemonSessionController extends EventEmitte
16638
16485
  sessionId,
16639
16486
  newState: "waiting_for_tool_confirmation" /* WaitingForToolConfirmation */
16640
16487
  });
16641
- this.askUserHandler.handleAskUserRequest(request, requestId, sessionId);
16488
+ this.askUserHandler.handleAskUserRequest(
16489
+ request,
16490
+ requestId,
16491
+ sessionId,
16492
+ {
16493
+ machineId: this.config.machineId
16494
+ }
16495
+ );
16642
16496
  void this.tryReplayBufferedAskUser(sessionId, requestId);
16643
16497
  }
16644
16498
  );
16645
- this.permissionHandler.on("permissionRequested", (permission) => {
16499
+ const onPermissionRequested = (permission) => {
16500
+ if (!this.machineOwnsPrompt(permission)) {
16501
+ return;
16502
+ }
16646
16503
  this.emit("permissionRequested" /* PermissionRequested */, permission);
16647
16504
  this.dispatchPermissionToSessionSubscribers(permission);
16648
- });
16649
- this.permissionHandler.on("permissionResolved", (id, option) => {
16505
+ };
16506
+ this.permissionHandler.on("permissionRequested", onPermissionRequested);
16507
+ this.promptStoreUnsubscribes.push(
16508
+ () => this.permissionHandler.off("permissionRequested", onPermissionRequested)
16509
+ );
16510
+ const onPermissionResolved = (id, option, context) => {
16511
+ if (context && !this.machineOwnsPrompt(context)) {
16512
+ return;
16513
+ }
16650
16514
  this.emit("permissionResolved" /* PermissionResolved */, id, option);
16651
- });
16652
- this.permissionHandler.on("permissionTimeout", (id) => {
16515
+ };
16516
+ this.permissionHandler.on("permissionResolved", onPermissionResolved);
16517
+ this.promptStoreUnsubscribes.push(
16518
+ () => this.permissionHandler.off("permissionResolved", onPermissionResolved)
16519
+ );
16520
+ const onPermissionTimeout = (id, context) => {
16521
+ if (context && !this.machineOwnsPrompt(context)) {
16522
+ return;
16523
+ }
16653
16524
  this.emit("permissionTimeout" /* PermissionTimeout */, id);
16654
- });
16655
- this.askUserHandler.on("askUserRequested", (req) => {
16525
+ };
16526
+ this.permissionHandler.on("permissionTimeout", onPermissionTimeout);
16527
+ this.promptStoreUnsubscribes.push(
16528
+ () => this.permissionHandler.off("permissionTimeout", onPermissionTimeout)
16529
+ );
16530
+ const onAskUserRequested = (req) => {
16531
+ if (!this.machineOwnsPrompt(req)) {
16532
+ return;
16533
+ }
16656
16534
  this.emit("askUserRequested" /* AskUserRequested */, req);
16657
- });
16658
- this.askUserHandler.on("askUserResolved", (id, result) => {
16535
+ };
16536
+ this.askUserHandler.on("askUserRequested", onAskUserRequested);
16537
+ this.promptStoreUnsubscribes.push(
16538
+ () => this.askUserHandler.off("askUserRequested", onAskUserRequested)
16539
+ );
16540
+ const onAskUserResolved = (id, result, context) => {
16541
+ if (context && !this.machineOwnsPrompt(context)) {
16542
+ return;
16543
+ }
16659
16544
  this.emit("askUserResolved" /* AskUserResolved */, id, result);
16660
- });
16545
+ };
16546
+ this.askUserHandler.on("askUserResolved", onAskUserResolved);
16547
+ this.promptStoreUnsubscribes.push(
16548
+ () => this.askUserHandler.off("askUserResolved", onAskUserResolved)
16549
+ );
16661
16550
  }
16662
16551
  handleWebSocketMessage(data) {
16663
16552
  try {
@@ -16931,7 +16820,7 @@ var DaemonSessionController = class _DaemonSessionController extends EventEmitte
16931
16820
  token,
16932
16821
  ...options?.actAsGrant && { actAsGrant: options.actAsGrant },
16933
16822
  ...connectionId && { connectionId },
16934
- caller: this.isDesktopApp() ? "web-desktop" /* WebDesktop */ : "web-app" /* WebApp */,
16823
+ caller: this.getFactoryClientType(),
16935
16824
  metadata: {
16936
16825
  tracing: {
16937
16826
  ...this.daemonClient.getTracingMetadata(),
@@ -16940,7 +16829,8 @@ var DaemonSessionController = class _DaemonSessionController extends EventEmitte
16940
16829
  connectAttemptId: this.connectAttemptContext.connectAttemptId,
16941
16830
  connectAttemptTrigger: this.connectAttemptContext.connectAttemptTrigger
16942
16831
  }
16943
- }
16832
+ },
16833
+ ...this.config.sdk && { sdk: this.config.sdk }
16944
16834
  }
16945
16835
  });
16946
16836
  try {
@@ -16962,7 +16852,7 @@ var DaemonSessionController = class _DaemonSessionController extends EventEmitte
16962
16852
  await this.daemonClient.logout();
16963
16853
  this._isAuthenticated = false;
16964
16854
  this.authenticationPromise = null;
16965
- this.permissionHandler.clearAllPermissions();
16855
+ this.clearPromptsForOwnedSessions();
16966
16856
  this.emit("connectionStatusChanged" /* StatusChanged */, this.getConnectionStatus());
16967
16857
  }
16968
16858
  disconnect() {
@@ -16977,10 +16867,33 @@ var DaemonSessionController = class _DaemonSessionController extends EventEmitte
16977
16867
  this.isReconnecting = false;
16978
16868
  this._isAuthenticated = false;
16979
16869
  this.authenticationPromise = null;
16980
- this.permissionHandler.clearAllPermissions();
16981
16870
  this.daemonClient.destroy();
16982
16871
  this.emit("connectionStatusChanged" /* StatusChanged */, this.getConnectionStatus());
16983
16872
  }
16873
+ /**
16874
+ * Clear pending prompts owned by this controller's machine, leaving other
16875
+ * machines' prompt state in the shared stores untouched.
16876
+ *
16877
+ * Ownership resolves from the machineId stamped on the record at intake,
16878
+ * falling back to the session's manager binding when absent — the same
16879
+ * identity the emission predicate uses, so a prompt this machine surfaced
16880
+ * (including one taken in while its session was still `''`-bound optimistic
16881
+ * or whose manager was since evicted) is cleared by this machine's logout.
16882
+ */
16883
+ clearPromptsForOwnedSessions() {
16884
+ for (const permission of this.permissionHandler.getPendingPermissions()) {
16885
+ if (this.getPromptOwnership(permission) === "owned") {
16886
+ this.permissionHandler.clearPermission(permission.requestId, {
16887
+ emitResolved: true
16888
+ });
16889
+ }
16890
+ }
16891
+ for (const request of this.askUserHandler.getPendingAskUserRequests()) {
16892
+ if (this.getPromptOwnership(request) === "owned") {
16893
+ this.askUserHandler.clearAskUser(request.requestId);
16894
+ }
16895
+ }
16896
+ }
16984
16897
  /**
16985
16898
  * Send `initialize_session` to the daemon, transparently retrying on
16986
16899
  * RequestTimeoutError up to INITIALIZE_SESSION_MAX_ATTEMPTS. Other errors
@@ -17072,7 +16985,7 @@ var DaemonSessionController = class _DaemonSessionController extends EventEmitte
17072
16985
  if (callingToolUseId) {
17073
16986
  sessionManager?.getStore().setCallingToolUseId(callingToolUseId);
17074
16987
  }
17075
- this.sessionLoadOptions.set(
16988
+ this.sessionStateManager.setSessionLoadOptions(
17076
16989
  result.sessionId,
17077
16990
  DaemonLoadSessionSpawnOptionsSchema.parse({
17078
16991
  disableInactivityTimeout: params.disableInactivityTimeout,
@@ -17168,7 +17081,7 @@ var DaemonSessionController = class _DaemonSessionController extends EventEmitte
17168
17081
  if (loadState !== "NOT_LOADED" /* NotLoaded */) {
17169
17082
  return;
17170
17083
  }
17171
- const stashed = this.sessionLoadOptions.get(sessionId) ?? {};
17084
+ const stashed = this.sessionStateManager.getSessionLoadOptions(sessionId) ?? {};
17172
17085
  await this.loadSession({
17173
17086
  ...stashed,
17174
17087
  sessionId,
@@ -17204,7 +17117,7 @@ var DaemonSessionController = class _DaemonSessionController extends EventEmitte
17204
17117
  if (loadState === "LOADING" /* Loading */) {
17205
17118
  return;
17206
17119
  }
17207
- const stashed = this.sessionLoadOptions.get(sessionId) ?? {};
17120
+ const stashed = this.sessionStateManager.getSessionLoadOptions(sessionId) ?? {};
17208
17121
  try {
17209
17122
  await this.loadSession({
17210
17123
  ...stashed,
@@ -17261,15 +17174,16 @@ var DaemonSessionController = class _DaemonSessionController extends EventEmitte
17261
17174
  params.sessionId,
17262
17175
  this.config.machineId
17263
17176
  );
17177
+ const workingStateRevisionBeforeLoad = this.sessionStateManager.getSessionManager(params.sessionId)?.getWorkingStateRevision() ?? 0;
17264
17178
  const fetched = await this.config.getAccessToken();
17265
17179
  if (fetched === null) {
17266
17180
  throw new MetaError("No access token available for loadSession", {
17267
17181
  sessionId: params.sessionId
17268
17182
  });
17269
17183
  }
17270
- const currentSpawnOptions = this.sessionLoadOptions.get(params.sessionId) ?? {};
17184
+ const stashedOptions = this.sessionStateManager.getSessionLoadOptions(params.sessionId) ?? {};
17271
17185
  const structuredOutputFormat = resolveStructuredOutputFormat(
17272
- currentSpawnOptions,
17186
+ stashedOptions,
17273
17187
  params
17274
17188
  );
17275
17189
  const resolvedParams = {
@@ -17278,9 +17192,12 @@ var DaemonSessionController = class _DaemonSessionController extends EventEmitte
17278
17192
  token: fetched,
17279
17193
  loadAllMessages: this.getClientType() === "cli" ? params.loadAllMessages : true,
17280
17194
  messageLimit: params.messageLimit ?? this.config.defaultMessageLimit,
17281
- disableInactivityTimeout: params.disableInactivityTimeout ?? this.config.disableInactivityTimeout
17195
+ disableInactivityTimeout: params.disableInactivityTimeout ?? stashedOptions.disableInactivityTimeout ?? this.config.disableInactivityTimeout,
17196
+ disableBuiltinSkills: params.disableBuiltinSkills ?? stashedOptions.disableBuiltinSkills,
17197
+ skipPermissionsUnsafe: params.skipPermissionsUnsafe ?? stashedOptions.skipPermissionsUnsafe,
17198
+ runtimeSettingsPath: params.runtimeSettingsPath ?? stashedOptions.runtimeSettingsPath
17282
17199
  };
17283
- this.sessionLoadOptions.set(
17200
+ this.sessionStateManager.setSessionLoadOptions(
17284
17201
  params.sessionId,
17285
17202
  DaemonLoadSessionSpawnOptionsSchema.parse({
17286
17203
  disableInactivityTimeout: resolvedParams.disableInactivityTimeout,
@@ -17291,10 +17208,18 @@ var DaemonSessionController = class _DaemonSessionController extends EventEmitte
17291
17208
  })
17292
17209
  );
17293
17210
  let result;
17211
+ let reportedWorkingState;
17212
+ let didHydrateReportedWorkingState = false;
17294
17213
  const loadPromise = this.daemonClient.loadSession(resolvedParams);
17295
17214
  this.daemonClient.setPendingSessionReady?.(params.sessionId, loadPromise);
17296
17215
  try {
17297
17216
  result = await loadPromise;
17217
+ reportedWorkingState = result.workingState !== void 0 && result.workingState !== "idle" /* Idle */ ? result.workingState : result.isAgentLoopInProgress ? "streaming_assistant_message" /* StreamingAssistantMessage */ : "idle" /* Idle */;
17218
+ const sessionManager2 = this.sessionStateManager.getSessionManager(
17219
+ params.sessionId
17220
+ );
17221
+ const shouldHydrateReportedWorkingState = sessionManager2?.getWorkingStateRevision() === workingStateRevisionBeforeLoad;
17222
+ didHydrateReportedWorkingState = shouldHydrateReportedWorkingState && sessionManager2?.getDroidWorkingState() !== reportedWorkingState;
17298
17223
  this.sessionStateManager.loadSession(
17299
17224
  params.sessionId,
17300
17225
  this.config.machineId,
@@ -17302,7 +17227,8 @@ var DaemonSessionController = class _DaemonSessionController extends EventEmitte
17302
17227
  {
17303
17228
  cwd: result.cwd ?? null,
17304
17229
  hasOlderMessages: result.hasOlderMessages ?? false,
17305
- loadedMessageLimit: resolvedParams.messageLimit ?? 0
17230
+ loadedMessageLimit: resolvedParams.messageLimit ?? 0,
17231
+ ...shouldHydrateReportedWorkingState ? { workingState: reportedWorkingState } : {}
17306
17232
  }
17307
17233
  );
17308
17234
  if (result.subagentInvocations && result.subagentInvocations.length > 0) {
@@ -17325,7 +17251,6 @@ var DaemonSessionController = class _DaemonSessionController extends EventEmitte
17325
17251
  const sessionManager = this.sessionStateManager.getSessionManager(
17326
17252
  params.sessionId
17327
17253
  );
17328
- const workingStateRevisionAtLoad = sessionManager?.getWorkingStateRevision() ?? 0;
17329
17254
  if (sessionManager && result.settings) {
17330
17255
  if (result.settings.modelId !== void 0) {
17331
17256
  sessionManager.getStore().setModelId(result.settings.modelId);
@@ -17427,7 +17352,7 @@ var DaemonSessionController = class _DaemonSessionController extends EventEmitte
17427
17352
  request.data,
17428
17353
  permissionRequest.requestId,
17429
17354
  params.sessionId,
17430
- { isRestored: true }
17355
+ { isRestored: true, machineId: this.config.machineId }
17431
17356
  );
17432
17357
  surfacedPermissionCount += 1;
17433
17358
  }
@@ -17501,7 +17426,7 @@ var DaemonSessionController = class _DaemonSessionController extends EventEmitte
17501
17426
  request.data,
17502
17427
  askUserRequest.requestId,
17503
17428
  params.sessionId,
17504
- { isRestored: true }
17429
+ { isRestored: true, machineId: this.config.machineId }
17505
17430
  );
17506
17431
  }
17507
17432
  logInfo(
@@ -17517,7 +17442,7 @@ var DaemonSessionController = class _DaemonSessionController extends EventEmitte
17517
17442
  );
17518
17443
  }
17519
17444
  } finally {
17520
- this.inactiveSessions.delete(params.sessionId);
17445
+ this.sessionStateManager.markSessionActive(params.sessionId);
17521
17446
  }
17522
17447
  if (sessionManager) {
17523
17448
  await reconcileQueuedMessagesAfterLoad({
@@ -17528,9 +17453,8 @@ var DaemonSessionController = class _DaemonSessionController extends EventEmitte
17528
17453
  });
17529
17454
  }
17530
17455
  const hasAssociatedPendingPermission = this.permissionHandler.getPendingPermissionsForSession(params.sessionId).length > 0;
17531
- const reportedWorkingState = result.workingState !== void 0 && result.workingState !== "idle" /* Idle */ ? result.workingState : result.isAgentLoopInProgress ? "streaming_assistant_message" /* StreamingAssistantMessage */ : "idle" /* Idle */;
17532
17456
  const loadedWorkingState = hasAssociatedPendingPermission || hasPendingAskUserRequests ? "waiting_for_tool_confirmation" /* WaitingForToolConfirmation */ : reportedWorkingState;
17533
- if (sessionManager && sessionManager.getWorkingStateRevision() !== workingStateRevisionAtLoad) {
17457
+ if (sessionManager && sessionManager.getWorkingStateRevision() !== workingStateRevisionBeforeLoad) {
17534
17458
  logInfo(
17535
17459
  "[DaemonSessionController] Skipping stale working state from load",
17536
17460
  {
@@ -17539,18 +17463,23 @@ var DaemonSessionController = class _DaemonSessionController extends EventEmitte
17539
17463
  state: sessionManager.getDroidWorkingState()
17540
17464
  }
17541
17465
  );
17542
- } else if (loadedWorkingState !== "idle" /* Idle */ || sessionManager && sessionManager.getDroidWorkingState() !== "idle" /* Idle */) {
17543
- sessionManager?.applyDroidWorkingState(loadedWorkingState);
17544
- this.emit("droidWorkingStateChanged" /* DroidWorkingStateChanged */, {
17545
- sessionId: params.sessionId,
17546
- newState: loadedWorkingState
17547
- });
17548
- logInfo("[DaemonSessionController] Restored working state on load", {
17549
- sessionId: params.sessionId,
17550
- value: loadedWorkingState,
17551
- pendingRequestCount: hasAssociatedPendingPermission ? 1 : 0,
17552
- questionCount: hasPendingAskUserRequests ? 1 : 0
17553
- });
17466
+ } else if (sessionManager) {
17467
+ const didApplyLoadedWorkingState = sessionManager.getDroidWorkingState() !== loadedWorkingState;
17468
+ if (didApplyLoadedWorkingState) {
17469
+ sessionManager.applyDroidWorkingState(loadedWorkingState);
17470
+ }
17471
+ if (loadedWorkingState !== "idle" /* Idle */ || didHydrateReportedWorkingState || didApplyLoadedWorkingState) {
17472
+ this.emit("droidWorkingStateChanged" /* DroidWorkingStateChanged */, {
17473
+ sessionId: params.sessionId,
17474
+ newState: loadedWorkingState
17475
+ });
17476
+ logInfo("[DaemonSessionController] Restored working state on load", {
17477
+ sessionId: params.sessionId,
17478
+ value: loadedWorkingState,
17479
+ pendingCount: hasAssociatedPendingPermission ? 1 : 0,
17480
+ totalQuestions: hasPendingAskUserRequests ? 1 : 0
17481
+ });
17482
+ }
17554
17483
  }
17555
17484
  if (this.config.supportsTerminalRestoreOnLoad !== false) {
17556
17485
  await this.loadTerminals(params.sessionId);
@@ -17634,6 +17563,7 @@ var DaemonSessionController = class _DaemonSessionController extends EventEmitte
17634
17563
  const wasOptimisticallySubmittedWhileIdleBeforeRequest = sessionManager?.getDroidWorkingState() === "idle" /* Idle */ && sessionManager.getOptimisticMessage(requestId) !== void 0;
17635
17564
  const content = buildUserMessageContentBlocks2({
17636
17565
  text: params.text,
17566
+ content: params.content,
17637
17567
  images: params.images,
17638
17568
  files: params.files
17639
17569
  });
@@ -17754,15 +17684,9 @@ var DaemonSessionController = class _DaemonSessionController extends EventEmitte
17754
17684
  const result = await this.daemonClient.interruptSession({
17755
17685
  sessionId
17756
17686
  });
17757
- const permissionsToClose = this.permissionHandler.getPendingPermissions().filter((p) => p.sessionId === sessionId);
17758
- this.permissionHandler.clearSessionPermissions(sessionId);
17759
- for (const permission of permissionsToClose) {
17760
- this.emit(
17761
- "permissionResolved" /* PermissionResolved */,
17762
- permission.requestId,
17763
- "cancel" /* Cancel */
17764
- );
17765
- }
17687
+ this.permissionHandler.clearSessionPermissions(sessionId, {
17688
+ emitResolved: true
17689
+ });
17766
17690
  const askUserToClose = this.askUserHandler.getPendingAskUserRequests().filter((r) => r.sessionId === sessionId);
17767
17691
  for (const req of askUserToClose) {
17768
17692
  try {
@@ -17806,7 +17730,7 @@ var DaemonSessionController = class _DaemonSessionController extends EventEmitte
17806
17730
  */
17807
17731
  async closeSession(sessionId) {
17808
17732
  await this.daemonClient.closeSession({ sessionId });
17809
- this.sessionLoadOptions.delete(sessionId);
17733
+ this.sessionStateManager.deleteSessionLoadOptions(sessionId);
17810
17734
  this.sessionLoadInFlight.delete(sessionId);
17811
17735
  this.trackedChildSessionIds.delete(sessionId);
17812
17736
  this.childSessionAttachmentGeneration.delete(sessionId);
@@ -17876,8 +17800,11 @@ var DaemonSessionController = class _DaemonSessionController extends EventEmitte
17876
17800
  async resolvePullRequestStatuses(params) {
17877
17801
  return await this.daemonClient.resolvePullRequestStatuses(params);
17878
17802
  }
17879
- async listGitBranches(cwd) {
17880
- return this.daemonClient.listGitBranches({ cwd });
17803
+ async listGitBranches(cwd, options) {
17804
+ return this.daemonClient.listGitBranches({
17805
+ cwd,
17806
+ ...options?.includeOriginBranches === true ? { includeOriginBranches: true } : {}
17807
+ });
17881
17808
  }
17882
17809
  async checkoutGitBranch(params) {
17883
17810
  return this.daemonClient.checkoutGitBranch(params);
@@ -18445,7 +18372,7 @@ var DaemonSessionController = class _DaemonSessionController extends EventEmitte
18445
18372
  async runSelfResumeWithRetry(sessionId) {
18446
18373
  let lastError;
18447
18374
  for (let attempt = 1; attempt <= SELF_RESUME_MAX_ATTEMPTS; attempt++) {
18448
- const stashed = this.sessionLoadOptions.get(sessionId) ?? {};
18375
+ const stashed = this.sessionStateManager.getSessionLoadOptions(sessionId) ?? {};
18449
18376
  try {
18450
18377
  await this.loadSession({
18451
18378
  sessionId,
@@ -18534,7 +18461,7 @@ var DaemonSessionController = class _DaemonSessionController extends EventEmitte
18534
18461
  }
18535
18462
  );
18536
18463
  }
18537
- if (this.inactiveSessions.has(executionSessionId)) {
18464
+ if (this.sessionStateManager.isSessionInactive(executionSessionId)) {
18538
18465
  const toolUseId = permission.toolUses[0]?.toolUse.id;
18539
18466
  if (toolUseId) {
18540
18467
  this.pendingUserActionStore.savePermission({
@@ -18593,7 +18520,7 @@ var DaemonSessionController = class _DaemonSessionController extends EventEmitte
18593
18520
  requestId
18594
18521
  });
18595
18522
  }
18596
- if (this.inactiveSessions.has(sessionId)) {
18523
+ if (this.sessionStateManager.isSessionInactive(sessionId)) {
18597
18524
  this.pendingUserActionStore.saveAskUser({
18598
18525
  kind: "askUser",
18599
18526
  sessionId,
@@ -18718,6 +18645,46 @@ var DaemonSessionController = class _DaemonSessionController extends EventEmitte
18718
18645
  getMachineId() {
18719
18646
  return this.config.machineId;
18720
18647
  }
18648
+ /**
18649
+ * Whether a prompt (or its resolution) involves this controller's machine.
18650
+ *
18651
+ * A prompt is owned by this machine when its stamped `machineId` (set at
18652
+ * intake by the controller that received it) matches, or when any involved
18653
+ * session's manager is bound to this machine (a relayed subagent prompt
18654
+ * surfaces on the parent's machine via `associatedSessionIds`). Sessions
18655
+ * with no registered manager (or no machine binding yet) don't count either
18656
+ * way; if the prompt carries no stamp and none of the sessions has a known
18657
+ * binding, this returns true so pre-registration edge cases keep the
18658
+ * pre-shared-store behavior of emitting.
18659
+ *
18660
+ * Public so the CLI's TuiDaemonAdapter can dedupe the legacy remote relay
18661
+ * against this controller's native shared-store fan-out.
18662
+ */
18663
+ machineOwnsPrompt(prompt) {
18664
+ const sessionIds = /* @__PURE__ */ new Set([
18665
+ prompt.sessionId,
18666
+ ...prompt.associatedSessionIds ?? []
18667
+ ]);
18668
+ const ownerships = [...sessionIds].map(
18669
+ (sessionId) => this.getPromptOwnership({
18670
+ sessionId,
18671
+ machineId: sessionId === prompt.sessionId ? prompt.machineId : void 0
18672
+ })
18673
+ );
18674
+ return ownerships.includes("owned") || ownerships.every((ownership) => ownership === "unknown");
18675
+ }
18676
+ /**
18677
+ * Resolve one prompt record or involved session against this controller.
18678
+ * Emission treats an all-unknown prompt as visible. Logout clears only an
18679
+ * exact owned record.
18680
+ */
18681
+ getPromptOwnership(prompt) {
18682
+ const machineId = prompt.machineId || this.sessionStateManager.getSessionManager(prompt.sessionId)?.getMachineId();
18683
+ if (!machineId) {
18684
+ return "unknown";
18685
+ }
18686
+ return machineId === this.config.machineId ? "owned" : "foreign";
18687
+ }
18721
18688
  /**
18722
18689
  * Get the machine type for this daemon client.
18723
18690
  */
@@ -18822,8 +18789,10 @@ var DaemonSessionController = class _DaemonSessionController extends EventEmitte
18822
18789
  this.disconnect();
18823
18790
  this.removeAllListeners();
18824
18791
  this.messageRouter.removeAllListeners();
18825
- this.permissionHandler.destroy();
18826
- this.askUserHandler.destroy();
18792
+ for (const unsubscribe of this.promptStoreUnsubscribes) {
18793
+ unsubscribe();
18794
+ }
18795
+ this.promptStoreUnsubscribes.length = 0;
18827
18796
  this.sessionPermissionSubscribers.clear();
18828
18797
  this.sessionLoadInFlight.clear();
18829
18798
  this.trackedChildSessionIds.clear();
@@ -18832,7 +18801,7 @@ var DaemonSessionController = class _DaemonSessionController extends EventEmitte
18832
18801
  };
18833
18802
 
18834
18803
  // ../daemon-sdk/src/session/ControllerEventAggregator.ts
18835
- import { EventEmitter as EventEmitter7 } from "eventemitter3";
18804
+ import { EventEmitter as EventEmitter5 } from "eventemitter3";
18836
18805
 
18837
18806
  // ../daemon-sdk/src/session/mcpStatus.ts
18838
18807
  function deriveMcpStatusFromSummary(summary) {
@@ -19131,7 +19100,7 @@ var TOOL_DISPLAY_NAMES = {
19131
19100
  [TOOL_LLM_ID_TODO_WRITE]: "Plan",
19132
19101
  [TOOL_LLM_ID_ASK_USER]: "Ask User",
19133
19102
  [TOOL_LLM_ID_WEB_SEARCH]: "Web Search",
19134
- [TOOL_LLM_ID_FETCH_URL]: "Fetch URL",
19103
+ [TOOL_LLM_ID_FETCH_URL]: "Web Fetch",
19135
19104
  [TOOL_LLM_ID_GENERATE_IMAGE]: "Generate Image",
19136
19105
  [TOOL_LLM_ID_GET_IDE_DIAGNOSTICS]: "Get IDE Diagnostics",
19137
19106
  [TOOL_LLM_ID_EXIT_SPEC_MODE]: "Propose Specification",
@@ -19140,7 +19109,7 @@ var TOOL_DISPLAY_NAMES = {
19140
19109
  [TOOL_LLM_ID_SELECT_FEATURE]: "Select Feature",
19141
19110
  [TOOL_LLM_ID_END_FEATURE_RUN]: "End Feature Run",
19142
19111
  [TOOL_LLM_ID_DISMISS_HANDOFF_ITEMS]: "Dismiss Handoff Items",
19143
- [TOOL_LLM_ID_TASK]: "Subagent",
19112
+ [TOOL_LLM_ID_TASK]: "Task",
19144
19113
  [TOOL_LLM_ID_CONNECTOR_SEARCH]: "Connectors",
19145
19114
  [TOOL_LLM_ID_CONNECT_APP]: "Connect App"
19146
19115
  };
@@ -20967,7 +20936,7 @@ var SessionStateManager = class _SessionStateManager {
20967
20936
  }
20968
20937
  return void 0;
20969
20938
  }
20970
- completeStreamingContentBlocks() {
20939
+ completeStreamingContentBlocks(options) {
20971
20940
  for (const message of this.store.getMessages()) {
20972
20941
  let didUpdateMessage = false;
20973
20942
  let thinkingBlockOrdinal = 0;
@@ -21001,10 +20970,14 @@ var SessionStateManager = class _SessionStateManager {
21001
20970
  return block;
21002
20971
  });
21003
20972
  if (didUpdateMessage) {
21004
- this.store.updateMessage(message.id, {
21005
- content,
21006
- updatedAt: Date.now()
21007
- });
20973
+ this.store.updateMessage(
20974
+ message.id,
20975
+ {
20976
+ content,
20977
+ updatedAt: Date.now()
20978
+ },
20979
+ options
20980
+ );
21008
20981
  }
21009
20982
  }
21010
20983
  this.streamingMessageIds.clear();
@@ -21259,9 +21232,9 @@ var SessionStateManager = class _SessionStateManager {
21259
21232
  /**
21260
21233
  * Set the load state of this session.
21261
21234
  */
21262
- setLoadState(state) {
21235
+ setLoadState(state, options) {
21263
21236
  this.loadState = state;
21264
- if (state === "NOT_LOADED" /* NotLoaded */ && this.droidWorkingState !== "idle" /* Idle */) {
21237
+ if (state === "NOT_LOADED" /* NotLoaded */ && options?.preserveWorkingState !== true && this.droidWorkingState !== "idle" /* Idle */) {
21265
21238
  this.stopStreaming();
21266
21239
  }
21267
21240
  }
@@ -21877,7 +21850,7 @@ var SessionStateManager = class _SessionStateManager {
21877
21850
  *
21878
21851
  * @param params - The notification parameters including sessionId and notification data
21879
21852
  */
21880
- handleNotification(params) {
21853
+ handleNotification(params, options) {
21881
21854
  const { notification } = params;
21882
21855
  const isStreamingDelta = notification.type === "assistant_text_delta" /* ASSISTANT_TEXT_DELTA */ || notification.type === "thinking_text_delta" /* THINKING_TEXT_DELTA */;
21883
21856
  const isBlockComplete = notification.type === "assistant_text_complete" /* ASSISTANT_TEXT_COMPLETE */ || notification.type === "thinking_text_complete" /* THINKING_TEXT_COMPLETE */;
@@ -21961,8 +21934,7 @@ var SessionStateManager = class _SessionStateManager {
21961
21934
  sessionId: params.sessionId,
21962
21935
  type: notification.errorType,
21963
21936
  errorName: notification.error?.name,
21964
- message: notification.message,
21965
- cause: notification.error
21937
+ message: notification.message
21966
21938
  });
21967
21939
  }
21968
21940
  this.stopStreaming();
@@ -21975,7 +21947,9 @@ var SessionStateManager = class _SessionStateManager {
21975
21947
  timestamp: notification.timestamp,
21976
21948
  timeout: notification.timeoutSeconds
21977
21949
  });
21978
- this.setLoadState("NOT_LOADED" /* NotLoaded */);
21950
+ this.setLoadState("NOT_LOADED" /* NotLoaded */, {
21951
+ preserveWorkingState: options?.preserveWorkingStateOnInactive === true
21952
+ });
21979
21953
  break;
21980
21954
  }
21981
21955
  case "session_process_exited" /* SESSION_PROCESS_EXITED */: {
@@ -21984,7 +21958,9 @@ var SessionStateManager = class _SessionStateManager {
21984
21958
  message: notification.message,
21985
21959
  timestamp: notification.timestamp
21986
21960
  });
21987
- this.setLoadState("NOT_LOADED" /* NotLoaded */);
21961
+ this.setLoadState("NOT_LOADED" /* NotLoaded */, {
21962
+ preserveWorkingState: options?.preserveWorkingStateOnInactive === true
21963
+ });
21988
21964
  break;
21989
21965
  }
21990
21966
  case "session_closed" /* SESSION_CLOSED */: {
@@ -22384,68 +22360,76 @@ var SessionStateManager = class _SessionStateManager {
22384
22360
  this.droidWorkingState = newState;
22385
22361
  this.droidWorkingStateChangedAtMs = Date.now();
22386
22362
  }
22363
+ transitionDroidWorkingState(newState, options) {
22364
+ const previousState = this.droidWorkingState;
22365
+ const transition = () => {
22366
+ if (newState !== "idle" /* Idle */) {
22367
+ this.store.clearAgentTurnCompletionReason();
22368
+ }
22369
+ switch (newState) {
22370
+ case "idle" /* Idle */:
22371
+ case "executing_tool" /* ExecutingTool */:
22372
+ case "waiting_for_tool_confirmation" /* WaitingForToolConfirmation */:
22373
+ this.completeStreamingContentBlocks({
22374
+ silent: options?.silent === true
22375
+ });
22376
+ break;
22377
+ case "thinking" /* Thinking */:
22378
+ case "streaming_assistant_message" /* StreamingAssistantMessage */:
22379
+ case "compacting_conversation" /* CompactingConversation */:
22380
+ break;
22381
+ default: {
22382
+ const exhaustiveCheck = newState;
22383
+ logError("Unknown droid working state:", { value: exhaustiveCheck });
22384
+ return;
22385
+ }
22386
+ }
22387
+ this.updateDroidWorkingState(newState);
22388
+ };
22389
+ if (options?.silent === true) {
22390
+ this.store.runWithStateEventsSuppressed(transition);
22391
+ } else {
22392
+ transition();
22393
+ }
22394
+ const didChange = previousState !== this.droidWorkingState;
22395
+ if (didChange && options?.silent !== true) {
22396
+ this.store.flushNotify("working_state_changed" /* WorkingStateChanged */);
22397
+ }
22398
+ }
22387
22399
  startStreaming() {
22388
- this.updateDroidWorkingState("streaming_assistant_message" /* StreamingAssistantMessage */);
22389
- this.store.clearAgentTurnCompletionReason();
22390
- this.store.flushNotify("working_state_changed" /* WorkingStateChanged */);
22400
+ this.transitionDroidWorkingState(
22401
+ "streaming_assistant_message" /* StreamingAssistantMessage */
22402
+ );
22391
22403
  }
22392
22404
  stopStreaming() {
22393
- this.updateDroidWorkingState("idle" /* Idle */);
22394
- this.completeStreamingContentBlocks();
22395
- this.store.flushNotify("working_state_changed" /* WorkingStateChanged */);
22405
+ this.transitionDroidWorkingState("idle" /* Idle */);
22396
22406
  }
22397
22407
  setWaitingForConfirmation() {
22398
- this.completeStreamingContentBlocks();
22399
- this.updateDroidWorkingState("waiting_for_tool_confirmation" /* WaitingForToolConfirmation */);
22400
- this.store.flushNotify("working_state_changed" /* WorkingStateChanged */);
22408
+ this.transitionDroidWorkingState(
22409
+ "waiting_for_tool_confirmation" /* WaitingForToolConfirmation */
22410
+ );
22401
22411
  }
22402
22412
  setExecutingTool() {
22403
- this.completeStreamingContentBlocks();
22404
- this.updateDroidWorkingState("executing_tool" /* ExecutingTool */);
22405
- this.store.flushNotify("working_state_changed" /* WorkingStateChanged */);
22413
+ this.transitionDroidWorkingState("executing_tool" /* ExecutingTool */);
22406
22414
  }
22407
22415
  setCompacting() {
22408
- this.updateDroidWorkingState("compacting_conversation" /* CompactingConversation */);
22409
- this.store.flushNotify("working_state_changed" /* WorkingStateChanged */);
22416
+ this.transitionDroidWorkingState("compacting_conversation" /* CompactingConversation */);
22410
22417
  }
22411
22418
  setThinking() {
22412
- this.updateDroidWorkingState("thinking" /* Thinking */);
22413
- this.store.flushNotify("working_state_changed" /* WorkingStateChanged */);
22419
+ this.transitionDroidWorkingState("thinking" /* Thinking */);
22414
22420
  }
22415
22421
  /**
22416
- * Apply a DroidWorkingState value to the local session state, routing to the
22417
- * matching setter. Used both by the working-state notification handler and by
22418
- * session load, where the load result reports the current state of an
22419
- * already-running session.
22422
+ * Apply a live DroidWorkingState value and notify subscribers when it changes.
22420
22423
  */
22421
22424
  applyDroidWorkingState(newState) {
22422
- if (newState !== "idle" /* Idle */) {
22423
- this.store.clearAgentTurnCompletionReason();
22424
- }
22425
- switch (newState) {
22426
- case "idle" /* Idle */:
22427
- this.stopStreaming();
22428
- break;
22429
- case "thinking" /* Thinking */:
22430
- this.setThinking();
22431
- break;
22432
- case "streaming_assistant_message" /* StreamingAssistantMessage */:
22433
- this.startStreaming();
22434
- break;
22435
- case "executing_tool" /* ExecutingTool */:
22436
- this.setExecutingTool();
22437
- break;
22438
- case "waiting_for_tool_confirmation" /* WaitingForToolConfirmation */:
22439
- this.setWaitingForConfirmation();
22440
- break;
22441
- case "compacting_conversation" /* CompactingConversation */:
22442
- this.setCompacting();
22443
- break;
22444
- default: {
22445
- const exhaustiveCheck = newState;
22446
- logError("Unknown droid working state:", { value: exhaustiveCheck });
22447
- }
22448
- }
22425
+ this.transitionDroidWorkingState(newState);
22426
+ }
22427
+ /**
22428
+ * Hydrate a load-response working state before message publication without
22429
+ * exposing an intermediate transcript/state pair to subscribers.
22430
+ */
22431
+ hydrateDroidWorkingState(newState) {
22432
+ this.transitionDroidWorkingState(newState, { silent: true });
22449
22433
  }
22450
22434
  /**
22451
22435
  * Queue a user prompt in SSM for shared UI rendering and lifecycle handling.
@@ -22662,325 +22646,763 @@ var SessionStateManager = class _SessionStateManager {
22662
22646
  this.queuedMessages = pausedQueue;
22663
22647
  this.store.notify("queued_messages_updated" /* QueuedMessagesUpdated */);
22664
22648
  }
22665
- clearQueue() {
22666
- this.queuedMessages.clear();
22667
- this.store.notify("queued_messages_updated" /* QueuedMessagesUpdated */);
22649
+ clearQueue() {
22650
+ this.queuedMessages.clear();
22651
+ this.store.notify("queued_messages_updated" /* QueuedMessagesUpdated */);
22652
+ }
22653
+ mergeOptimisticMessages(realMessages) {
22654
+ const entries = Array.from(this.optimisticMessages.values()).sort(
22655
+ (a, b) => (a.messages[0]?.createdAt ?? 0) - (b.messages[0]?.createdAt ?? 0)
22656
+ );
22657
+ const historicalSystemMessages = entries.filter(
22658
+ (entry) => entry.messages[0]?.role === "system" /* System */ && entry.leadingOrder === void 0
22659
+ ).flatMap((entry) => entry.messages);
22660
+ const leadingMessages = entries.filter((entry) => entry.leadingOrder !== void 0).sort((a, b) => (a.leadingOrder ?? 0) - (b.leadingOrder ?? 0)).flatMap((entry) => entry.messages);
22661
+ const pendingMessages = entries.filter(
22662
+ (entry) => entry.messages[0]?.role !== "system" /* System */ && entry.leadingOrder === void 0
22663
+ ).flatMap((entry) => entry.messages);
22664
+ const leadingInsertIndex = leadingMessages.length === 0 ? 0 : this.findLeadingInsertIndex(realMessages);
22665
+ return [
22666
+ ...historicalSystemMessages,
22667
+ ...realMessages.slice(0, leadingInsertIndex),
22668
+ ...leadingMessages,
22669
+ ...realMessages.slice(leadingInsertIndex),
22670
+ ...pendingMessages
22671
+ ];
22672
+ }
22673
+ /**
22674
+ * Where the leading optimistic cards render: after the last real message
22675
+ * that is one of their recorded siblings, or ahead of the transcript when
22676
+ * none has been recorded yet.
22677
+ */
22678
+ findLeadingInsertIndex(realMessages) {
22679
+ if (this.confirmedLeadingMessageIds.size === 0) {
22680
+ return 0;
22681
+ }
22682
+ let insertIndex = 0;
22683
+ realMessages.forEach((message, index) => {
22684
+ if (this.confirmedLeadingMessageIds.has(message.id)) {
22685
+ insertIndex = index + 1;
22686
+ }
22687
+ });
22688
+ return insertIndex;
22689
+ }
22690
+ /**
22691
+ * Note that `messageId` is the recorded form of a leading optimistic card,
22692
+ * so `findLeadingInsertIndex` can keep the cards that have not been recorded
22693
+ * yet below it. Ignores request ids that never staged a leading card.
22694
+ */
22695
+ rememberConfirmedLeadingMessage(requestId, messageId) {
22696
+ if (this.optimisticMessages.get(requestId)?.leadingOrder === void 0) {
22697
+ return;
22698
+ }
22699
+ this.confirmedLeadingMessageIds.add(messageId);
22700
+ }
22701
+ /**
22702
+ * Get messages filtered for UI display.
22703
+ * Uses the canonical filterMessagesForUI utility to ensure consistent filtering.
22704
+ * Includes optimistic messages that are pending server confirmation.
22705
+ */
22706
+ getDisplayMessages() {
22707
+ const sorted = this.mergeOptimisticMessages(
22708
+ filterMessagesForUI2(this.store.getMessages())
22709
+ );
22710
+ if (this.renderLimit !== null && sorted.length > this.renderLimit) {
22711
+ return sorted.slice(-this.renderLimit);
22712
+ }
22713
+ return sorted;
22714
+ }
22715
+ /**
22716
+ * Add an optimistic message that will be shown until confirmed by server.
22717
+ *
22718
+ * `leadingOrder` renders the message above the transcript instead of among
22719
+ * the pending turns, at the position that number gives it among the other
22720
+ * leading messages. Session-launch steps use it so their cards sit where
22721
+ * the daemon records the real ones: ahead of the first user message.
22722
+ */
22723
+ addOptimisticMessage(requestId, message, options) {
22724
+ this.addOptimisticMessages(requestId, [message], options);
22725
+ }
22726
+ /**
22727
+ * Replaces an optimistic exchange atomically so a tool result can settle
22728
+ * its staged tool use without briefly removing or separating the pair.
22729
+ */
22730
+ addOptimisticMessages(requestId, messages, options) {
22731
+ if (messages.length === 0) {
22732
+ return;
22733
+ }
22734
+ this.optimisticMessages.set(requestId, {
22735
+ messages,
22736
+ ...options?.leadingOrder !== void 0 ? { leadingOrder: options.leadingOrder } : {}
22737
+ });
22738
+ this.store.notifyMessagesChanged();
22739
+ }
22740
+ /**
22741
+ * Inspect a single optimistic message by requestId. Used by the
22742
+ * post-load reconciliation pass in `MultiSessionStateManager` to
22743
+ * match optimistic submits against the daemon-persisted user
22744
+ * messages and confirm them when the originating CREATE_MESSAGE
22745
+ * notification was lost.
22746
+ */
22747
+ getOptimisticMessage(requestId) {
22748
+ return this.optimisticMessages.get(requestId)?.messages[0];
22749
+ }
22750
+ /** Keeps the unconfirmed half of a staged exchange until it arrives. */
22751
+ confirmOptimisticMessages(requestId, messageId) {
22752
+ const entry = this.optimisticMessages.get(requestId);
22753
+ if (!entry) {
22754
+ return;
22755
+ }
22756
+ const containsConfirmedMessage = entry.messages.some(
22757
+ (message) => message.id === messageId
22758
+ );
22759
+ const remainingMessages = entry.messages.filter(
22760
+ (message) => message.id !== messageId
22761
+ );
22762
+ if (containsConfirmedMessage && remainingMessages.length > 0) {
22763
+ this.optimisticMessages.set(requestId, {
22764
+ ...entry,
22765
+ messages: remainingMessages
22766
+ });
22767
+ return;
22768
+ }
22769
+ this.optimisticMessages.delete(requestId);
22770
+ }
22771
+ /**
22772
+ * Remove optimistic message when confirmed or on error
22773
+ */
22774
+ removeOptimisticMessage(requestId) {
22775
+ if (this.optimisticMessages.delete(requestId)) {
22776
+ this.store.notifyMessagesChanged();
22777
+ }
22778
+ }
22779
+ /**
22780
+ * Update session settings (model ID, reasoning effort, and autonomy level)
22781
+ */
22782
+ updateSessionSettings(params) {
22783
+ if (params.modelId !== void 0) {
22784
+ this.store.setModelId(params.modelId);
22785
+ }
22786
+ if (params.reasoningEffort !== void 0) {
22787
+ this.store.setReasoningEffort(params.reasoningEffort);
22788
+ }
22789
+ this.applyInteractionSettings(params);
22790
+ if (params.tags !== void 0) {
22791
+ this.store.setTags(params.tags);
22792
+ }
22793
+ }
22794
+ /**
22795
+ * Resolve and apply interaction settings (interactionMode, autonomyLevel)
22796
+ * to the session store.
22797
+ */
22798
+ applyInteractionSettings(settings) {
22799
+ if (settings.interactionMode === void 0 && settings.autonomyLevel === void 0) {
22800
+ return;
22801
+ }
22802
+ const nextInteractionSettings = resolveInteractionSettings({
22803
+ interactionMode: settings.interactionMode,
22804
+ autonomyLevel: settings.autonomyLevel,
22805
+ fallback: {
22806
+ interactionMode: this.store.getInteractionMode() ?? "auto" /* Auto */,
22807
+ autonomyLevel: this.store.getAutonomyLevel() ?? "off" /* Off */
22808
+ }
22809
+ });
22810
+ this.store.setInteractionMode(nextInteractionSettings.interactionMode);
22811
+ this.store.setAutonomyLevel(nextInteractionSettings.autonomyLevel);
22812
+ }
22813
+ /**
22814
+ * Get current interaction mode
22815
+ */
22816
+ getInteractionMode() {
22817
+ return this.store.getInteractionMode();
22818
+ }
22819
+ /**
22820
+ * Get current autonomy level
22821
+ */
22822
+ getAutonomyLevel() {
22823
+ return this.store.getAutonomyLevel();
22824
+ }
22825
+ /**
22826
+ * Get current model ID
22827
+ */
22828
+ getModelId() {
22829
+ return this.store.getModelId();
22830
+ }
22831
+ /**
22832
+ * Get current reasoning effort
22833
+ */
22834
+ getReasoningEffort() {
22835
+ return this.store.getReasoningEffort();
22836
+ }
22837
+ /**
22838
+ * Get spec mode model ID
22839
+ */
22840
+ getSpecModeModelId() {
22841
+ return this.store.getSpecModeModelId();
22842
+ }
22843
+ /**
22844
+ * Get spec mode reasoning effort
22845
+ */
22846
+ getSpecModeReasoningEffort() {
22847
+ return this.store.getSpecModeReasoningEffort();
22668
22848
  }
22669
- mergeOptimisticMessages(realMessages) {
22670
- const entries = Array.from(this.optimisticMessages.values()).sort(
22671
- (a, b) => (a.messages[0]?.createdAt ?? 0) - (b.messages[0]?.createdAt ?? 0)
22672
- );
22673
- const historicalSystemMessages = entries.filter(
22674
- (entry) => entry.messages[0]?.role === "system" /* System */ && entry.leadingOrder === void 0
22675
- ).flatMap((entry) => entry.messages);
22676
- const leadingMessages = entries.filter((entry) => entry.leadingOrder !== void 0).sort((a, b) => (a.leadingOrder ?? 0) - (b.leadingOrder ?? 0)).flatMap((entry) => entry.messages);
22677
- const pendingMessages = entries.filter(
22678
- (entry) => entry.messages[0]?.role !== "system" /* System */ && entry.leadingOrder === void 0
22679
- ).flatMap((entry) => entry.messages);
22680
- const leadingInsertIndex = leadingMessages.length === 0 ? 0 : this.findLeadingInsertIndex(realMessages);
22681
- return [
22682
- ...historicalSystemMessages,
22683
- ...realMessages.slice(0, leadingInsertIndex),
22684
- ...leadingMessages,
22685
- ...realMessages.slice(leadingInsertIndex),
22686
- ...pendingMessages
22687
- ];
22849
+ /**
22850
+ * Get available models
22851
+ */
22852
+ getAvailableModels() {
22853
+ return this.store.getAvailableModels();
22688
22854
  }
22855
+ // ============ Terminal Registry Methods ============
22689
22856
  /**
22690
- * Where the leading optimistic cards render: after the last real message
22691
- * that is one of their recorded siblings, or ahead of the transcript when
22692
- * none has been recorded yet.
22857
+ * Register a mounted terminal's write handler.
22858
+ * Also flushes any buffered data that arrived while unmounted.
22693
22859
  */
22694
- findLeadingInsertIndex(realMessages) {
22695
- if (this.confirmedLeadingMessageIds.size === 0) {
22696
- return 0;
22860
+ registerTerminal(terminalId, writeHandler) {
22861
+ this.terminalWriteHandlers.set(terminalId, writeHandler);
22862
+ const buffered = this.store.getTerminalBufferedData(terminalId);
22863
+ if (buffered) {
22864
+ logInfo("[SessionStateManager] Flushing buffered terminal data", {
22865
+ terminalId
22866
+ });
22867
+ writeHandler(buffered);
22868
+ this.store.clearTerminalBufferedData(terminalId);
22697
22869
  }
22698
- let insertIndex = 0;
22699
- realMessages.forEach((message, index) => {
22700
- if (this.confirmedLeadingMessageIds.has(message.id)) {
22701
- insertIndex = index + 1;
22702
- }
22870
+ }
22871
+ /**
22872
+ * Unregister when terminal unmounts.
22873
+ * Future DATA notifications will be buffered.
22874
+ */
22875
+ unregisterTerminal(terminalId) {
22876
+ this.terminalWriteHandlers.delete(terminalId);
22877
+ logInfo("[SessionStateManager] Terminal unregistered", { terminalId });
22878
+ }
22879
+ /**
22880
+ * Store serialized terminal state (called on unmount)
22881
+ */
22882
+ storeTerminalState(terminalId, state) {
22883
+ this.store.storeTerminalSerializedState(terminalId, state);
22884
+ logInfo("[SessionStateManager] Terminal state serialized", {
22885
+ terminalId
22703
22886
  });
22704
- return insertIndex;
22705
22887
  }
22706
22888
  /**
22707
- * Note that `messageId` is the recorded form of a leading optimistic card,
22708
- * so `findLeadingInsertIndex` can keep the cards that have not been recorded
22709
- * yet below it. Ignores request ids that never staged a leading card.
22889
+ * Get serialized state for restoration
22710
22890
  */
22711
- rememberConfirmedLeadingMessage(requestId, messageId) {
22712
- if (this.optimisticMessages.get(requestId)?.leadingOrder === void 0) {
22713
- return;
22714
- }
22715
- this.confirmedLeadingMessageIds.add(messageId);
22891
+ getTerminalSerializedState(terminalId) {
22892
+ return this.store.getTerminalSerializedState(terminalId);
22716
22893
  }
22717
22894
  /**
22718
- * Get messages filtered for UI display.
22719
- * Uses the canonical filterMessagesForUI utility to ensure consistent filtering.
22720
- * Includes optimistic messages that are pending server confirmation.
22895
+ * Get buffered data for restoration
22721
22896
  */
22722
- getDisplayMessages() {
22723
- const sorted = this.mergeOptimisticMessages(
22724
- filterMessagesForUI2(this.store.getMessages())
22725
- );
22726
- if (this.renderLimit !== null && sorted.length > this.renderLimit) {
22727
- return sorted.slice(-this.renderLimit);
22728
- }
22729
- return sorted;
22897
+ getTerminalBufferedData(terminalId) {
22898
+ return this.store.getTerminalBufferedData(terminalId);
22730
22899
  }
22731
22900
  /**
22732
- * Add an optimistic message that will be shown until confirmed by server.
22733
- *
22734
- * `leadingOrder` renders the message above the transcript instead of among
22735
- * the pending turns, at the position that number gives it among the other
22736
- * leading messages. Session-launch steps use it so their cards sit where
22737
- * the daemon records the real ones: ahead of the first user message.
22901
+ * Clear only buffered data (called when daemon state is restored)
22738
22902
  */
22739
- addOptimisticMessage(requestId, message, options) {
22740
- this.addOptimisticMessages(requestId, [message], options);
22903
+ clearTerminalBufferedData(terminalId) {
22904
+ this.store.clearTerminalBufferedData(terminalId);
22741
22905
  }
22742
22906
  /**
22743
- * Replaces an optimistic exchange atomically so a tool result can settle
22744
- * its staged tool use without briefly removing or separating the pair.
22907
+ * Clear restoration state after terminal is restored
22745
22908
  */
22746
- addOptimisticMessages(requestId, messages, options) {
22747
- if (messages.length === 0) {
22748
- return;
22749
- }
22750
- this.optimisticMessages.set(requestId, {
22751
- messages,
22752
- ...options?.leadingOrder !== void 0 ? { leadingOrder: options.leadingOrder } : {}
22909
+ clearTerminalRestorationState(terminalId) {
22910
+ this.store.clearTerminalRestorationState(terminalId);
22911
+ logInfo("[SessionStateManager] Terminal restoration state cleared", {
22912
+ terminalId
22753
22913
  });
22754
- this.store.notifyMessagesChanged();
22755
22914
  }
22756
22915
  /**
22757
- * Inspect a single optimistic message by requestId. Used by the
22758
- * post-load reconciliation pass in `MultiSessionStateManager` to
22759
- * match optimistic submits against the daemon-persisted user
22760
- * messages and confirm them when the originating CREATE_MESSAGE
22761
- * notification was lost.
22916
+ * Get the machine ID for this session
22762
22917
  */
22763
- getOptimisticMessage(requestId) {
22764
- return this.optimisticMessages.get(requestId)?.messages[0];
22918
+ getMachineId() {
22919
+ return this.store.getMachineId();
22765
22920
  }
22766
- /** Keeps the unconfirmed half of a staged exchange until it arrives. */
22767
- confirmOptimisticMessages(requestId, messageId) {
22768
- const entry = this.optimisticMessages.get(requestId);
22769
- if (!entry) {
22921
+ /**
22922
+ * Set the machine ID for this session
22923
+ */
22924
+ setMachineId(machineId) {
22925
+ this.store.setMachineId(machineId);
22926
+ }
22927
+ // ============ UI Render Cutoff ============
22928
+ setUiRenderCutoff(messageId) {
22929
+ this.store.setUiRenderCutoff(messageId);
22930
+ if (messageId) {
22931
+ logInfo("[SessionStateManager] UI render cutoff set", {
22932
+ cutoffMessageId: messageId
22933
+ });
22934
+ }
22935
+ }
22936
+ getUiRenderCutoff() {
22937
+ return this.store.getUiRenderCutoff();
22938
+ }
22939
+ getUiRenderHiddenMessageCount() {
22940
+ return this.store.getUiRenderHiddenMessageCount();
22941
+ }
22942
+ /**
22943
+ * Double the render limit (or expand fully if doubling exceeds total count).
22944
+ * Returns true if there are still more messages to expand, false if fully
22945
+ * expanded. No-op unless progressive UI rendering is enabled.
22946
+ */
22947
+ expandUiMessages() {
22948
+ if (this.renderLimit === null) {
22949
+ return false;
22950
+ }
22951
+ const displayCount = filterMessagesForUI2(this.store.getMessages()).length;
22952
+ const nextLimit = this.renderLimit * 2;
22953
+ if (nextLimit >= displayCount) {
22954
+ this.renderLimit = null;
22955
+ } else {
22956
+ this.renderLimit = nextLimit;
22957
+ }
22958
+ this.store.notifyMessagesChanged();
22959
+ return this.renderLimit !== null;
22960
+ }
22961
+ addMessage(message, options) {
22962
+ this.applyTodoStateFromMessage(message);
22963
+ this.store.addMessage(message, options);
22964
+ }
22965
+ // ============ Conversation State Queries ============
22966
+ isConversationEmpty() {
22967
+ return this.store.getMessageCount() === 0 && this.optimisticMessages.size === 0;
22968
+ }
22969
+ };
22970
+
22971
+ // ../daemon-sdk/src/session/handlers/AskUserRequestHandler.ts
22972
+ import { EventEmitter as EventEmitter6 } from "eventemitter3";
22973
+ var AskUserRequestHandler = class _AskUserRequestHandler extends EventEmitter6 {
22974
+ pendingRequests = /* @__PURE__ */ new Map();
22975
+ static buildResolutionContext(pending) {
22976
+ return {
22977
+ sessionId: pending.sessionId,
22978
+ associatedSessionIds: [pending.sessionId],
22979
+ ...pending.machineId ? { machineId: pending.machineId } : {}
22980
+ };
22981
+ }
22982
+ handleAskUserRequest(request, id, sessionId, context) {
22983
+ logInfo("[AskUserRequestHandler] Handling ask-user request", {
22984
+ requestId: id,
22985
+ sessionId,
22986
+ toolCallId: request.params.toolCallId,
22987
+ count: request.params.questions.length
22988
+ });
22989
+ const machineId = context?.machineId ?? this.pendingRequests.get(id)?.machineId;
22990
+ const pending = {
22991
+ requestId: id,
22992
+ sessionId,
22993
+ toolCallId: request.params.toolCallId,
22994
+ questions: request.params.questions,
22995
+ timestamp: Date.now(),
22996
+ ...context?.isRestored ? { isRestored: true } : {},
22997
+ ...machineId ? { machineId } : {}
22998
+ };
22999
+ const existingRequest = this.pendingRequests.get(id);
23000
+ if (existingRequest) {
23001
+ this.pendingRequests.set(id, pending);
23002
+ logInfo("[AskUserRequestHandler] Duplicate ask-user request replay", {
23003
+ requestId: id,
23004
+ sessionId
23005
+ });
22770
23006
  return;
22771
23007
  }
22772
- const containsConfirmedMessage = entry.messages.some(
22773
- (message) => message.id === messageId
23008
+ this.pendingRequests.set(id, pending);
23009
+ this.emit("askUserRequested", pending);
23010
+ }
23011
+ resolveAskUser(id, result) {
23012
+ const pending = this.pendingRequests.get(id);
23013
+ if (!pending) {
23014
+ logWarn("[AskUserRequestHandler] No pending ask-user request found", {
23015
+ requestId: id
23016
+ });
23017
+ return;
23018
+ }
23019
+ _AskUserRequestHandler.validateResult(pending, result);
23020
+ this.pendingRequests.delete(id);
23021
+ this.emit(
23022
+ "askUserResolved",
23023
+ id,
23024
+ result,
23025
+ _AskUserRequestHandler.buildResolutionContext(pending)
22774
23026
  );
22775
- const remainingMessages = entry.messages.filter(
22776
- (message) => message.id !== messageId
23027
+ }
23028
+ clearAskUser(id) {
23029
+ const pending = this.pendingRequests.get(id);
23030
+ if (!pending) {
23031
+ logWarn(
23032
+ "[AskUserRequestHandler] No pending ask-user request found to clear",
23033
+ {
23034
+ requestId: id
23035
+ }
23036
+ );
23037
+ return;
23038
+ }
23039
+ this.pendingRequests.delete(id);
23040
+ this.emit(
23041
+ "askUserResolved",
23042
+ id,
23043
+ void 0,
23044
+ _AskUserRequestHandler.buildResolutionContext(pending)
23045
+ );
23046
+ }
23047
+ clearSessionAskUserRequests(sessionId) {
23048
+ const requestsToRemove = Array.from(this.pendingRequests.values()).filter(
23049
+ (request) => request.sessionId === sessionId
22777
23050
  );
22778
- if (containsConfirmedMessage && remainingMessages.length > 0) {
22779
- this.optimisticMessages.set(requestId, {
22780
- ...entry,
22781
- messages: remainingMessages
22782
- });
22783
- return;
23051
+ for (const request of requestsToRemove) {
23052
+ this.clearAskUser(request.requestId);
22784
23053
  }
22785
- this.optimisticMessages.delete(requestId);
22786
23054
  }
22787
- /**
22788
- * Remove optimistic message when confirmed or on error
22789
- */
22790
- removeOptimisticMessage(requestId) {
22791
- if (this.optimisticMessages.delete(requestId)) {
22792
- this.store.notifyMessagesChanged();
22793
- }
23055
+ cancelAskUser(id) {
23056
+ this.resolveAskUser(id, { cancelled: true, answers: [] });
22794
23057
  }
22795
- /**
22796
- * Update session settings (model ID, reasoning effort, and autonomy level)
22797
- */
22798
- updateSessionSettings(params) {
22799
- if (params.modelId !== void 0) {
22800
- this.store.setModelId(params.modelId);
23058
+ getPendingAskUserRequests() {
23059
+ return Array.from(this.pendingRequests.values());
23060
+ }
23061
+ getPendingAskUserRequest(id) {
23062
+ return this.pendingRequests.get(id);
23063
+ }
23064
+ clearAll() {
23065
+ this.pendingRequests.clear();
23066
+ }
23067
+ destroy() {
23068
+ this.clearAll();
23069
+ this.removeAllListeners();
23070
+ }
23071
+ static validateResult(pending, result) {
23072
+ if (result.cancelled === true) {
23073
+ return;
22801
23074
  }
22802
- if (params.reasoningEffort !== void 0) {
22803
- this.store.setReasoningEffort(params.reasoningEffort);
23075
+ const questionsByIndex = new Map(
23076
+ pending.questions.map((q) => [q.index, q])
23077
+ );
23078
+ const answered = /* @__PURE__ */ new Set();
23079
+ for (const answer of result.answers) {
23080
+ const question = questionsByIndex.get(answer.index);
23081
+ if (!question) {
23082
+ throw new MetaError("Answer references unknown question index", {
23083
+ requestId: pending.requestId,
23084
+ index: answer.index
23085
+ });
23086
+ }
23087
+ if (answered.has(answer.index)) {
23088
+ throw new MetaError("Duplicate answers for question index", {
23089
+ requestId: pending.requestId,
23090
+ index: answer.index
23091
+ });
23092
+ }
23093
+ answered.add(answer.index);
22804
23094
  }
22805
- this.applyInteractionSettings(params);
22806
- if (params.tags !== void 0) {
22807
- this.store.setTags(params.tags);
23095
+ const missing = pending.questions.map((q) => q.index).filter((idx) => !answered.has(idx));
23096
+ if (missing.length > 0) {
23097
+ throw new MetaError("Missing answers for some questions", {
23098
+ requestId: pending.requestId,
23099
+ missing: missing.map(String)
23100
+ });
22808
23101
  }
22809
23102
  }
22810
- /**
22811
- * Resolve and apply interaction settings (interactionMode, autonomyLevel)
22812
- * to the session store.
22813
- */
22814
- applyInteractionSettings(settings) {
22815
- if (settings.interactionMode === void 0 && settings.autonomyLevel === void 0) {
22816
- return;
23103
+ };
23104
+
23105
+ // ../daemon-sdk/src/session/handlers/PendingUserActionStore.ts
23106
+ var PendingUserActionStore = class {
23107
+ permissionsByToolUseId = /* @__PURE__ */ new Map();
23108
+ askUsersBySession = /* @__PURE__ */ new Map();
23109
+ savePermission(action) {
23110
+ this.permissionsByToolUseId.set(action.toolUseId, action);
23111
+ logInfo("[PendingUserActionStore] Stored permission for later replay", {
23112
+ sessionId: action.sessionId,
23113
+ requestId: action.requestId,
23114
+ toolUseId: action.toolUseId,
23115
+ selectedOptionLabel: action.selectedOption
23116
+ });
23117
+ }
23118
+ saveAskUser(action) {
23119
+ let map = this.askUsersBySession.get(action.sessionId);
23120
+ if (!map) {
23121
+ map = /* @__PURE__ */ new Map();
23122
+ this.askUsersBySession.set(action.sessionId, map);
22817
23123
  }
22818
- const nextInteractionSettings = resolveInteractionSettings({
22819
- interactionMode: settings.interactionMode,
22820
- autonomyLevel: settings.autonomyLevel,
22821
- fallback: {
22822
- interactionMode: this.store.getInteractionMode() ?? "auto" /* Auto */,
22823
- autonomyLevel: this.store.getAutonomyLevel() ?? "off" /* Off */
22824
- }
23124
+ map.set(action.toolCallId, action);
23125
+ logInfo("[PendingUserActionStore] Stored ask-user for later replay", {
23126
+ sessionId: action.sessionId,
23127
+ requestId: action.requestId,
23128
+ toolCallId: action.toolCallId
22825
23129
  });
22826
- this.store.setInteractionMode(nextInteractionSettings.interactionMode);
22827
- this.store.setAutonomyLevel(nextInteractionSettings.autonomyLevel);
22828
23130
  }
22829
- /**
22830
- * Get current interaction mode
22831
- */
22832
- getInteractionMode() {
22833
- return this.store.getInteractionMode();
23131
+ /** Peek a buffered permission response by its globally-unique tool-use id. */
23132
+ getForToolUse(toolUseId) {
23133
+ return this.permissionsByToolUseId.get(toolUseId);
22834
23134
  }
22835
- /**
22836
- * Get current autonomy level
22837
- */
22838
- getAutonomyLevel() {
22839
- return this.store.getAutonomyLevel();
23135
+ getForToolCall(sessionId, toolCallId) {
23136
+ return this.askUsersBySession.get(sessionId)?.get(toolCallId);
22840
23137
  }
22841
- /**
22842
- * Get current model ID
22843
- */
22844
- getModelId() {
22845
- return this.store.getModelId();
23138
+ /** Remove and return a buffered permission response by tool-use id. */
23139
+ takeForToolUse(toolUseId) {
23140
+ const action = this.permissionsByToolUseId.get(toolUseId);
23141
+ if (!action) return void 0;
23142
+ this.permissionsByToolUseId.delete(toolUseId);
23143
+ return action;
22846
23144
  }
22847
- /**
22848
- * Get current reasoning effort
22849
- */
22850
- getReasoningEffort() {
22851
- return this.store.getReasoningEffort();
23145
+ takeForToolCall(sessionId, toolCallId) {
23146
+ const map = this.askUsersBySession.get(sessionId);
23147
+ if (!map) return void 0;
23148
+ const action = map.get(toolCallId);
23149
+ if (!action) return void 0;
23150
+ map.delete(toolCallId);
23151
+ if (map.size === 0) this.askUsersBySession.delete(sessionId);
23152
+ return action;
22852
23153
  }
22853
- /**
22854
- * Get spec mode model ID
22855
- */
22856
- getSpecModeModelId() {
22857
- return this.store.getSpecModeModelId();
23154
+ getPermissionsForSession(sessionId) {
23155
+ return Array.from(this.permissionsByToolUseId.values()).filter(
23156
+ (action) => action.sessionId === sessionId
23157
+ );
22858
23158
  }
22859
- /**
22860
- * Get spec mode reasoning effort
22861
- */
22862
- getSpecModeReasoningEffort() {
22863
- return this.store.getSpecModeReasoningEffort();
23159
+ getAskUsersForSession(sessionId) {
23160
+ const map = this.askUsersBySession.get(sessionId);
23161
+ return map ? Array.from(map.values()) : [];
23162
+ }
23163
+ clearSession(sessionId) {
23164
+ let permCount = 0;
23165
+ for (const [toolUseId, action] of this.permissionsByToolUseId) {
23166
+ if (action.sessionId === sessionId) {
23167
+ this.permissionsByToolUseId.delete(toolUseId);
23168
+ permCount += 1;
23169
+ }
23170
+ }
23171
+ const askCount = this.askUsersBySession.get(sessionId)?.size ?? 0;
23172
+ this.askUsersBySession.delete(sessionId);
23173
+ const total = permCount + askCount;
23174
+ if (total > 0) {
23175
+ logInfo("[PendingUserActionStore] Cleared session", {
23176
+ sessionId,
23177
+ pendingCount: permCount,
23178
+ totalQuestions: askCount
23179
+ });
23180
+ }
23181
+ return total;
23182
+ }
23183
+ clearAll() {
23184
+ this.permissionsByToolUseId.clear();
23185
+ this.askUsersBySession.clear();
23186
+ }
23187
+ destroy() {
23188
+ this.clearAll();
22864
23189
  }
23190
+ };
23191
+
23192
+ // ../daemon-sdk/src/session/handlers/PermissionRequestHandler.ts
23193
+ import { EventEmitter as EventEmitter7 } from "eventemitter3";
23194
+ function normalizeAssociatedSessionIds(sessionId, associatedSessionIds) {
23195
+ return Array.from(/* @__PURE__ */ new Set([sessionId, ...associatedSessionIds ?? []]));
23196
+ }
23197
+ var PermissionRequestHandler = class _PermissionRequestHandler extends EventEmitter7 {
23198
+ pendingPermissions = /* @__PURE__ */ new Map();
23199
+ statistics = {
23200
+ total: 0,
23201
+ approved: 0,
23202
+ denied: 0,
23203
+ timedOut: 0
23204
+ };
22865
23205
  /**
22866
- * Get available models
23206
+ * Handle an incoming permission request
22867
23207
  */
22868
- getAvailableModels() {
22869
- return this.store.getAvailableModels();
23208
+ static buildResolutionContext(pending) {
23209
+ return {
23210
+ sessionId: pending.sessionId,
23211
+ associatedSessionIds: pending.associatedSessionIds,
23212
+ ...pending.machineId ? { machineId: pending.machineId } : {}
23213
+ };
23214
+ }
23215
+ handlePermissionRequest(request, id, sessionId, context) {
23216
+ logInfo("[PermissionRequestHandler] Handling permission request", {
23217
+ requestId: id,
23218
+ sessionId,
23219
+ count: request.params.toolUses.length,
23220
+ options: request.params.options
23221
+ });
23222
+ const { options } = request.params;
23223
+ const toolUses = request.params.toolUses.map((tool) => ({
23224
+ ...tool,
23225
+ toolUse: {
23226
+ ...tool.toolUse,
23227
+ input: getPermissionToolInputForDisplay2({
23228
+ confirmationType: tool.confirmationType,
23229
+ details: tool.details,
23230
+ toolInput: tool.toolUse.input
23231
+ })
23232
+ }
23233
+ }));
23234
+ const requestAssociatedSessionIds = normalizeAssociatedSessionIds(
23235
+ sessionId,
23236
+ request.params.associatedSessionIds
23237
+ );
23238
+ const existingPermission = this.pendingPermissions.get(id);
23239
+ const associatedSessionIds = existingPermission ? normalizeAssociatedSessionIds(sessionId, [
23240
+ ...existingPermission.associatedSessionIds,
23241
+ ...requestAssociatedSessionIds
23242
+ ]) : requestAssociatedSessionIds;
23243
+ const machineId = context?.machineId ?? existingPermission?.machineId;
23244
+ const pendingPermission = {
23245
+ requestId: id,
23246
+ sessionId,
23247
+ associatedSessionIds,
23248
+ toolUses,
23249
+ options,
23250
+ timestamp: Date.now(),
23251
+ ...context?.isRestored ? { isRestored: true } : {},
23252
+ ...machineId ? { machineId } : {}
23253
+ };
23254
+ if (existingPermission) {
23255
+ this.pendingPermissions.set(id, pendingPermission);
23256
+ const addedSurface = associatedSessionIds.some(
23257
+ (associatedSessionId) => !existingPermission.associatedSessionIds.includes(associatedSessionId)
23258
+ );
23259
+ logInfo(
23260
+ "[PermissionRequestHandler] Duplicate permission request replay",
23261
+ {
23262
+ requestId: id,
23263
+ sessionId
23264
+ }
23265
+ );
23266
+ if (addedSurface) {
23267
+ this.emit("permissionRequested", pendingPermission);
23268
+ }
23269
+ return;
23270
+ }
23271
+ const incomingToolUseIds = new Set(
23272
+ toolUses.map((toolUse) => toolUse.toolUse.id)
23273
+ );
23274
+ for (const [pendingId, pending] of this.pendingPermissions.entries()) {
23275
+ if (pending.machineId && machineId && pending.machineId !== machineId) {
23276
+ continue;
23277
+ }
23278
+ if (pending.toolUses.some(
23279
+ (toolUse) => incomingToolUseIds.has(toolUse.toolUse.id)
23280
+ )) {
23281
+ this.clearPermission(pendingId, { emitResolved: true });
23282
+ }
23283
+ }
23284
+ this.pendingPermissions.set(id, pendingPermission);
23285
+ this.statistics.total++;
23286
+ logInfo("[PermissionRequestHandler] Stored pending permission", {
23287
+ requestId: id,
23288
+ count: this.pendingPermissions.size
23289
+ });
23290
+ logInfo("[PermissionRequestHandler] Emitting permissionRequested event", {
23291
+ requestId: id
23292
+ });
23293
+ this.emit("permissionRequested", pendingPermission);
22870
23294
  }
22871
- // ============ Terminal Registry Methods ============
22872
23295
  /**
22873
- * Register a mounted terminal's write handler.
22874
- * Also flushes any buffered data that arrived while unmounted.
23296
+ * Resolve a permission request with user's choice
22875
23297
  */
22876
- registerTerminal(terminalId, writeHandler) {
22877
- this.terminalWriteHandlers.set(terminalId, writeHandler);
22878
- const buffered = this.store.getTerminalBufferedData(terminalId);
22879
- if (buffered) {
22880
- logInfo("[SessionStateManager] Flushing buffered terminal data", {
22881
- terminalId
22882
- });
22883
- writeHandler(buffered);
22884
- this.store.clearTerminalBufferedData(terminalId);
23298
+ resolvePermission(id, option) {
23299
+ const pending = this.pendingPermissions.get(id);
23300
+ if (!pending) {
23301
+ throw new MetaError("No pending permission found", { requestId: id });
23302
+ }
23303
+ const isValidOption = pending.options.some((opt) => opt.value === option);
23304
+ if (!isValidOption) {
23305
+ const allowedOptions = pending.options.map((opt) => opt.value);
23306
+ throw new MetaError(
23307
+ `Invalid permission option: ${option}. Allowed: ${allowedOptions.join(", ")}`,
23308
+ {
23309
+ requestId: id
23310
+ }
23311
+ );
23312
+ }
23313
+ this.pendingPermissions.delete(id);
23314
+ if (option !== "cancel" /* Cancel */) {
23315
+ this.statistics.approved++;
23316
+ } else {
23317
+ this.statistics.denied++;
22885
23318
  }
23319
+ this.emit(
23320
+ "permissionResolved",
23321
+ id,
23322
+ option,
23323
+ _PermissionRequestHandler.buildResolutionContext(pending)
23324
+ );
22886
23325
  }
22887
23326
  /**
22888
- * Unregister when terminal unmounts.
22889
- * Future DATA notifications will be buffered.
23327
+ * Clear a specific permission without an outcome.
22890
23328
  */
22891
- unregisterTerminal(terminalId) {
22892
- this.terminalWriteHandlers.delete(terminalId);
22893
- logInfo("[SessionStateManager] Terminal unregistered", { terminalId });
23329
+ clearPermission(id, options) {
23330
+ const pending = this.pendingPermissions.get(id);
23331
+ this.pendingPermissions.delete(id);
23332
+ if (options?.emitResolved) {
23333
+ this.emit(
23334
+ "permissionResolved",
23335
+ id,
23336
+ void 0,
23337
+ pending ? _PermissionRequestHandler.buildResolutionContext(pending) : void 0
23338
+ );
23339
+ }
22894
23340
  }
22895
23341
  /**
22896
- * Store serialized terminal state (called on unmount)
23342
+ * Clear all pending permissions for a specific session.
23343
+ *
23344
+ * With `preserveRelayed`, permissions surfaced on multiple sessions (a
23345
+ * relayed subagent permission whose `associatedSessionIds` extend beyond this
23346
+ * session) are kept. loadSession uses this so reloading the parent doesn't
23347
+ * wipe a child's still-pending prompt that the daemon's parent-session load
23348
+ * never returns.
22897
23349
  */
22898
- storeTerminalState(terminalId, state) {
22899
- this.store.storeTerminalSerializedState(terminalId, state);
22900
- logInfo("[SessionStateManager] Terminal state serialized", {
22901
- terminalId
23350
+ clearSessionPermissions(sessionId, options) {
23351
+ const permissionsToRemove = [];
23352
+ for (const [id, permission] of this.pendingPermissions.entries()) {
23353
+ if (permission.sessionId !== sessionId) {
23354
+ continue;
23355
+ }
23356
+ if (options?.preserveRelayed && permission.associatedSessionIds.some(
23357
+ (associatedId) => associatedId !== sessionId
23358
+ )) {
23359
+ continue;
23360
+ }
23361
+ permissionsToRemove.push(id);
23362
+ }
23363
+ for (const id of permissionsToRemove) {
23364
+ this.clearPermission(id, options);
23365
+ }
23366
+ logInfo("[PermissionRequestHandler] Cleared permissions for session", {
23367
+ sessionId,
23368
+ count: permissionsToRemove.length
22902
23369
  });
22903
23370
  }
22904
23371
  /**
22905
- * Get serialized state for restoration
22906
- */
22907
- getTerminalSerializedState(terminalId) {
22908
- return this.store.getTerminalSerializedState(terminalId);
22909
- }
22910
- /**
22911
- * Get buffered data for restoration
23372
+ * Clear all pending permissions
22912
23373
  */
22913
- getTerminalBufferedData(terminalId) {
22914
- return this.store.getTerminalBufferedData(terminalId);
23374
+ clearAllPermissions() {
23375
+ this.pendingPermissions.clear();
22915
23376
  }
22916
23377
  /**
22917
- * Clear only buffered data (called when daemon state is restored)
23378
+ * Get all pending permissions
22918
23379
  */
22919
- clearTerminalBufferedData(terminalId) {
22920
- this.store.clearTerminalBufferedData(terminalId);
23380
+ getPendingPermissions() {
23381
+ return Array.from(this.pendingPermissions.values());
22921
23382
  }
22922
- /**
22923
- * Clear restoration state after terminal is restored
22924
- */
22925
- clearTerminalRestorationState(terminalId) {
22926
- this.store.clearTerminalRestorationState(terminalId);
22927
- logInfo("[SessionStateManager] Terminal restoration state cleared", {
22928
- terminalId
22929
- });
23383
+ getPendingPermissionsForSession(sessionId) {
23384
+ return this.getPendingPermissions().filter(
23385
+ (permission) => permission.associatedSessionIds.includes(sessionId)
23386
+ );
22930
23387
  }
22931
23388
  /**
22932
- * Get the machine ID for this session
23389
+ * Get a specific pending permission
22933
23390
  */
22934
- getMachineId() {
22935
- return this.store.getMachineId();
23391
+ getPendingPermission(id) {
23392
+ return this.pendingPermissions.get(id);
22936
23393
  }
22937
23394
  /**
22938
- * Set the machine ID for this session
23395
+ * Check if there are any pending permissions
22939
23396
  */
22940
- setMachineId(machineId) {
22941
- this.store.setMachineId(machineId);
22942
- }
22943
- // ============ UI Render Cutoff ============
22944
- setUiRenderCutoff(messageId) {
22945
- this.store.setUiRenderCutoff(messageId);
22946
- if (messageId) {
22947
- logInfo("[SessionStateManager] UI render cutoff set", {
22948
- cutoffMessageId: messageId
22949
- });
22950
- }
22951
- }
22952
- getUiRenderCutoff() {
22953
- return this.store.getUiRenderCutoff();
22954
- }
22955
- getUiRenderHiddenMessageCount() {
22956
- return this.store.getUiRenderHiddenMessageCount();
23397
+ hasPendingPermissions() {
23398
+ return this.pendingPermissions.size > 0;
22957
23399
  }
22958
23400
  /**
22959
- * Double the render limit (or expand fully if doubling exceeds total count).
22960
- * Returns true if there are still more messages to expand, false if fully
22961
- * expanded. No-op unless progressive UI rendering is enabled.
23401
+ * Destroy and clean up
22962
23402
  */
22963
- expandUiMessages() {
22964
- if (this.renderLimit === null) {
22965
- return false;
22966
- }
22967
- const displayCount = filterMessagesForUI2(this.store.getMessages()).length;
22968
- const nextLimit = this.renderLimit * 2;
22969
- if (nextLimit >= displayCount) {
22970
- this.renderLimit = null;
22971
- } else {
22972
- this.renderLimit = nextLimit;
22973
- }
22974
- this.store.notifyMessagesChanged();
22975
- return this.renderLimit !== null;
22976
- }
22977
- addMessage(message, options) {
22978
- this.applyTodoStateFromMessage(message);
22979
- this.store.addMessage(message, options);
22980
- }
22981
- // ============ Conversation State Queries ============
22982
- isConversationEmpty() {
22983
- return this.store.getMessageCount() === 0 && this.optimisticMessages.size === 0;
23403
+ destroy() {
23404
+ this.clearAllPermissions();
23405
+ this.removeAllListeners();
22984
23406
  }
22985
23407
  };
22986
23408
 
@@ -23006,6 +23428,43 @@ var MultiSessionStateManager = class _MultiSessionStateManager {
23006
23428
  subagentInvocationSummaries = /* @__PURE__ */ new Map();
23007
23429
  // Track sessions that have already had their initial prompt sent (persists across remounts)
23008
23430
  sentInitialPromptSessions = /* @__PURE__ */ new Set();
23431
+ // Session-scoped prompt state, owned here (app lifetime) rather than by the
23432
+ // per-connection DaemonSessionController: pending prompts and buffered
23433
+ // answers must survive controller teardown/recreation (machine removal,
23434
+ // act-as recreation, provider remount), which otherwise wiped them while
23435
+ // loadState stayed Loaded and suppressed the daemon-side restore. Entries
23436
+ // are keyed by globally unique request ids and carry their sessionId, so a
23437
+ // single shared instance serves every machine's controller.
23438
+ _askUserRequests = new AskUserRequestHandler();
23439
+ _permissionRequests = new PermissionRequestHandler();
23440
+ _pendingUserActions = new PendingUserActionStore();
23441
+ // Narrowed public views: per-entry read/subscribe/mutate only. Whole-store
23442
+ // teardown stays internal (see removeSession / clear).
23443
+ get askUserRequests() {
23444
+ return this._askUserRequests;
23445
+ }
23446
+ get permissionRequests() {
23447
+ return this._permissionRequests;
23448
+ }
23449
+ get pendingUserActions() {
23450
+ return this._pendingUserActions;
23451
+ }
23452
+ // Session-scoped state that describes the *session* rather than the
23453
+ // connection, moved off the per-connection DaemonSessionController so a
23454
+ // replacement controller (preconnect round-trip, reconnect, machine
23455
+ // removal) does not lose it:
23456
+ // - inactiveSessions: whether the daemon paused the session's worker; pairs
23457
+ // with the pendingUserActions buffering above so respondToPermission /
23458
+ // respondToAskUser keep taking the buffer-and-self-resume path.
23459
+ // - sessionsWithConcurrentPrompts: bookkeeping over the prompt stores above;
23460
+ // must live and die with them so a rebuilt controller does not blanket
23461
+ // clear the surviving tail prompt of a concurrent batch.
23462
+ // - sessionLoadOptions: spawn options replayed on any later
23463
+ // ensureSessionLoaded / self-resume so the session is not respawned with
23464
+ // the wrong flags.
23465
+ inactiveSessions = /* @__PURE__ */ new Set();
23466
+ sessionsWithConcurrentPrompts = /* @__PURE__ */ new Set();
23467
+ sessionLoadOptions = /* @__PURE__ */ new Map();
23009
23468
  onToolProgressUpdate;
23010
23469
  isProgressiveUiRenderingEnabled;
23011
23470
  // Store for explicit setting changes made before a concrete session store
@@ -23053,19 +23512,21 @@ var MultiSessionStateManager = class _MultiSessionStateManager {
23053
23512
  emitSessionStateEvent = (event, payload) => {
23054
23513
  for (const listener of this.sessionStateEvents.listeners(event)) {
23055
23514
  try {
23056
- listener(payload);
23515
+ Reflect.apply(listener, void 0, [payload]);
23057
23516
  } catch (error) {
23058
23517
  logWarn("[MultiSessionStateManager] Error in state event listener", {
23059
23518
  error
23060
23519
  });
23061
23520
  }
23062
23521
  }
23522
+ if (event === "working_state_changed" /* WorkingStateChanged */ && this.sessions.size > this.maxCapacity && this.sessions.get(payload.sessionId)?.getDroidWorkingState() === "idle" /* Idle */) {
23523
+ this.evictIfNeeded();
23524
+ }
23063
23525
  };
23064
23526
  subscribeToSessionEvent(event, listener) {
23065
- const handler = listener;
23066
- this.sessionStateEvents.on(event, handler);
23527
+ this.sessionStateEvents.on(event, listener);
23067
23528
  return () => {
23068
- this.sessionStateEvents.off(event, handler);
23529
+ this.sessionStateEvents.off(event, listener);
23069
23530
  };
23070
23531
  }
23071
23532
  subscribeToSessionEvents(events, listener) {
@@ -23148,9 +23609,7 @@ var MultiSessionStateManager = class _MultiSessionStateManager {
23148
23609
  count: this.sessions.size,
23149
23610
  limit: this.maxCapacity
23150
23611
  });
23151
- this.removeSession(evictionTarget);
23152
- this.pendingNotifications.delete(evictionTarget);
23153
- this.lastAccessTime.delete(evictionTarget);
23612
+ this.evictCachedSession(evictionTarget);
23154
23613
  }
23155
23614
  }
23156
23615
  /**
@@ -23235,7 +23694,7 @@ var MultiSessionStateManager = class _MultiSessionStateManager {
23235
23694
  * Drain any notifications that arrived for `sessionId` before its session
23236
23695
  * manager was registered, replaying them in arrival order.
23237
23696
  */
23238
- replayPendingNotifications(sessionId, manager, phase) {
23697
+ replayPendingNotifications(sessionId, phase) {
23239
23698
  const queued = this.pendingNotifications.get(sessionId);
23240
23699
  if (!queued || queued.length === 0) return;
23241
23700
  logInfo(
@@ -23245,9 +23704,8 @@ var MultiSessionStateManager = class _MultiSessionStateManager {
23245
23704
  count: queued.length
23246
23705
  }
23247
23706
  );
23248
- queued.forEach((params) => {
23249
- manager.handleNotification(params);
23250
- this.maybeConfirmOptimisticSubmit(params);
23707
+ queued.forEach(({ params, options }) => {
23708
+ this.handleNotification(params, options);
23251
23709
  });
23252
23710
  this.pendingNotifications.delete(sessionId);
23253
23711
  }
@@ -23495,7 +23953,7 @@ var MultiSessionStateManager = class _MultiSessionStateManager {
23495
23953
  logInfo("[MultiSessionStateManager] Setting machineId for session", {
23496
23954
  sessionId,
23497
23955
  machineId,
23498
- previousState: manager.getMachineId() ?? ""
23956
+ state: manager.getMachineId() ?? ""
23499
23957
  });
23500
23958
  manager.setMachineId(machineId);
23501
23959
  this.emitStateChange(sessionId, manager.getLoadState());
@@ -23520,7 +23978,7 @@ var MultiSessionStateManager = class _MultiSessionStateManager {
23520
23978
  if (previousState !== "LOADED" /* Loaded */) {
23521
23979
  this.emitStateChange(sessionId, "LOADED" /* Loaded */);
23522
23980
  }
23523
- this.replayPendingNotifications(sessionId, manager, "init");
23981
+ this.replayPendingNotifications(sessionId, "init");
23524
23982
  if (this.sessions.size > this.maxCapacity) {
23525
23983
  this.evictIfNeeded();
23526
23984
  }
@@ -23531,16 +23989,17 @@ var MultiSessionStateManager = class _MultiSessionStateManager {
23531
23989
  */
23532
23990
  loadSession(sessionId, machineId, messages, options) {
23533
23991
  _MultiSessionStateManager.validateSessionId(sessionId);
23534
- const { manager, isNew } = this.getOrCreateSessionManager(
23535
- sessionId,
23536
- machineId
23537
- );
23992
+ const { manager } = this.getOrCreateSessionManager(sessionId, machineId);
23538
23993
  const previousState = manager.getLoadState();
23539
23994
  const priorMessageIds = new Set(
23540
23995
  manager.getMessages().map((message) => message.id)
23541
23996
  );
23542
23997
  manager.setHasOlderMessages(options?.hasOlderMessages ?? false);
23543
23998
  manager.setLoadedMessageLimit(options?.loadedMessageLimit ?? 0);
23999
+ const workingState = options?.workingState;
24000
+ if (workingState !== void 0 && manager.getDroidWorkingState() !== workingState) {
24001
+ manager.hydrateDroidWorkingState(workingState);
24002
+ }
23544
24003
  manager.loadSession(sessionId, messages);
23545
24004
  if (options && "cwd" in options) {
23546
24005
  manager.getStore().setCwd(options.cwd ?? null);
@@ -23553,8 +24012,8 @@ var MultiSessionStateManager = class _MultiSessionStateManager {
23553
24012
  messages,
23554
24013
  priorMessageIds
23555
24014
  );
23556
- this.replayPendingNotifications(sessionId, manager, "load");
23557
- if (isNew) {
24015
+ this.replayPendingNotifications(sessionId, "load");
24016
+ if (this.sessions.size > this.maxCapacity) {
23558
24017
  this.evictIfNeeded();
23559
24018
  }
23560
24019
  }
@@ -23617,7 +24076,7 @@ var MultiSessionStateManager = class _MultiSessionStateManager {
23617
24076
  if (shouldSeedStreaming) {
23618
24077
  manager.startStreaming();
23619
24078
  }
23620
- this.replayPendingNotifications(childSessionId, manager, "load");
24079
+ this.replayPendingNotifications(childSessionId, "load");
23621
24080
  }
23622
24081
  /**
23623
24082
  * Get the SessionStateManager for a specific session.
@@ -23822,10 +24281,42 @@ var MultiSessionStateManager = class _MultiSessionStateManager {
23822
24281
  return this.sessions.get(sessionId)?.getCurrentTodos() ?? null;
23823
24282
  }
23824
24283
  /**
23825
- * Remove a session from management.
24284
+ * Remove a session from management when it is definitively gone (closed,
24285
+ * not found).
24286
+ *
24287
+ * Pending prompts can never be answered, so they are cleared with
24288
+ * resolution events; the durable session-scoped collections (inactive
24289
+ * flag, concurrent-prompt marker, load options) go with them; and buffered
24290
+ * answers in pendingUserActions are dropped too, because no future load or
24291
+ * SESSION_CLOSED can confirm or drain them once the session is gone.
24292
+ *
24293
+ * Contrast {@link evictCachedSession}, which drops only the LRU cache
24294
+ * entities while the daemon still holds the session.
23826
24295
  */
23827
24296
  removeSession(sessionId) {
23828
24297
  _MultiSessionStateManager.validateSessionId(sessionId);
24298
+ this.evictCachedSession(sessionId);
24299
+ this._askUserRequests.clearSessionAskUserRequests(sessionId);
24300
+ this._permissionRequests.clearSessionPermissions(sessionId, {
24301
+ emitResolved: true
24302
+ });
24303
+ this._pendingUserActions.clearSession(sessionId);
24304
+ this.inactiveSessions.delete(sessionId);
24305
+ this.sessionsWithConcurrentPrompts.delete(sessionId);
24306
+ this.sessionLoadOptions.delete(sessionId);
24307
+ }
24308
+ /**
24309
+ * Drop only the LRU cache entities for a session (render-cache manager,
24310
+ * access time, queued notifications) under cache pressure, leaving the
24311
+ * durable session-scoped state intact: pending prompts, the inactive flag,
24312
+ * the concurrent-prompt marker, stashed load options, and buffered answers
24313
+ * all survive.
24314
+ *
24315
+ * The daemon still holds an evicted session and may be blocked on a pending
24316
+ * prompt, so a later re-open (or a response from another surface) still
24317
+ * resolves it and a self-resume replays the real load flags.
24318
+ */
24319
+ evictCachedSession(sessionId) {
23829
24320
  const session = this.sessions.get(sessionId);
23830
24321
  if (session) {
23831
24322
  session.getStore().clearAll();
@@ -23853,6 +24344,12 @@ var MultiSessionStateManager = class _MultiSessionStateManager {
23853
24344
  this.lastAccessTime.clear();
23854
24345
  this.subagentInvocationSummaries.clear();
23855
24346
  this.preInitSessions.clear();
24347
+ this._askUserRequests.clearAll();
24348
+ this._permissionRequests.clearAllPermissions();
24349
+ this._pendingUserActions.clearAll();
24350
+ this.inactiveSessions.clear();
24351
+ this.sessionsWithConcurrentPrompts.clear();
24352
+ this.sessionLoadOptions.clear();
23856
24353
  }
23857
24354
  /**
23858
24355
  * Handle notifications and route them to the appropriate session.
@@ -23861,7 +24358,7 @@ var MultiSessionStateManager = class _MultiSessionStateManager {
23861
24358
  * race conditions where notifications arrive before initializeSession/loadSession.
23862
24359
  * Queued notifications are replayed after session initialization.
23863
24360
  */
23864
- handleNotification(params) {
24361
+ handleNotification(params, options) {
23865
24362
  const { sessionId } = params;
23866
24363
  try {
23867
24364
  _MultiSessionStateManager.validateSessionId(sessionId);
@@ -23883,12 +24380,12 @@ var MultiSessionStateManager = class _MultiSessionStateManager {
23883
24380
  if (!this.pendingNotifications.has(sessionId)) {
23884
24381
  this.pendingNotifications.set(sessionId, []);
23885
24382
  }
23886
- this.pendingNotifications.get(sessionId).push(params);
24383
+ this.pendingNotifications.get(sessionId).push({ params, options });
23887
24384
  return;
23888
24385
  }
23889
24386
  const previousState = manager.getLoadState();
23890
24387
  const previousWorkingState = params.notification.type === "droid_working_state_changed" /* DROID_WORKING_STATE_CHANGED */ ? manager.getDroidWorkingState() : void 0;
23891
- manager.handleNotification(params);
24388
+ manager.handleNotification(params, options);
23892
24389
  this.maybeConfirmOptimisticSubmit(params);
23893
24390
  const newState = manager.getLoadState();
23894
24391
  if (previousState !== newState) {
@@ -24087,6 +24584,75 @@ var MultiSessionStateManager = class _MultiSessionStateManager {
24087
24584
  _MultiSessionStateManager.validateSessionId(sessionId);
24088
24585
  return this.sentInitialPromptSessions.has(sessionId);
24089
24586
  }
24587
+ /**
24588
+ * Mark a session's worker as paused (inactivity timeout / process exit).
24589
+ * While inactive, prompt responses are buffered in pendingUserActions and
24590
+ * the owning controller self-resumes instead of firing RPCs at a reaped
24591
+ * worker.
24592
+ */
24593
+ markSessionInactive(sessionId) {
24594
+ _MultiSessionStateManager.validateSessionId(sessionId);
24595
+ this.inactiveSessions.add(sessionId);
24596
+ }
24597
+ /**
24598
+ * Clear a session's inactive marker (a successful reload has resumed it).
24599
+ */
24600
+ markSessionActive(sessionId) {
24601
+ _MultiSessionStateManager.validateSessionId(sessionId);
24602
+ this.inactiveSessions.delete(sessionId);
24603
+ }
24604
+ /**
24605
+ * Whether a session's worker is currently paused.
24606
+ */
24607
+ isSessionInactive(sessionId) {
24608
+ _MultiSessionStateManager.validateSessionId(sessionId);
24609
+ return this.inactiveSessions.has(sessionId);
24610
+ }
24611
+ /**
24612
+ * Mark that a session is draining a batch of concurrent prompts, so a
24613
+ * resume-driven auto-clear does not strand the surviving tail prompt.
24614
+ */
24615
+ markConcurrentPrompts(sessionId) {
24616
+ _MultiSessionStateManager.validateSessionId(sessionId);
24617
+ this.sessionsWithConcurrentPrompts.add(sessionId);
24618
+ }
24619
+ /**
24620
+ * Forget a session's concurrent-prompt marker once its batch fully drains.
24621
+ */
24622
+ clearConcurrentPrompts(sessionId) {
24623
+ _MultiSessionStateManager.validateSessionId(sessionId);
24624
+ this.sessionsWithConcurrentPrompts.delete(sessionId);
24625
+ }
24626
+ /**
24627
+ * Whether a session is mid-way through draining a concurrent prompt batch.
24628
+ */
24629
+ hasConcurrentPrompts(sessionId) {
24630
+ _MultiSessionStateManager.validateSessionId(sessionId);
24631
+ return this.sessionsWithConcurrentPrompts.has(sessionId);
24632
+ }
24633
+ /**
24634
+ * Stash the spawn options used to load a session so they can be replayed on
24635
+ * a later ensureSessionLoaded / self-resume by whichever controller loads
24636
+ * the session next.
24637
+ */
24638
+ setSessionLoadOptions(sessionId, options) {
24639
+ _MultiSessionStateManager.validateSessionId(sessionId);
24640
+ this.sessionLoadOptions.set(sessionId, options);
24641
+ }
24642
+ /**
24643
+ * Read the stashed spawn options for a session, if any.
24644
+ */
24645
+ getSessionLoadOptions(sessionId) {
24646
+ _MultiSessionStateManager.validateSessionId(sessionId);
24647
+ return this.sessionLoadOptions.get(sessionId);
24648
+ }
24649
+ /**
24650
+ * Drop the stashed spawn options for a session (e.g. on close).
24651
+ */
24652
+ deleteSessionLoadOptions(sessionId) {
24653
+ _MultiSessionStateManager.validateSessionId(sessionId);
24654
+ this.sessionLoadOptions.delete(sessionId);
24655
+ }
24090
24656
  };
24091
24657
 
24092
24658
  // ../daemon-sdk/src/session/DaemonConnectionManager.ts
@@ -24570,7 +25136,7 @@ var PublicResumeSessionParamsSchema = LoadSessionRequestParamsSchema2.pick({
24570
25136
  });
24571
25137
  function createSdkSessionAttribution(sessionId) {
24572
25138
  return {
24573
- sessionOriginHint: "api" /* Api */,
25139
+ sessionOriginHint: "sdk" /* Sdk */,
24574
25140
  sessionSource: {
24575
25141
  platform: "api" /* Api */,
24576
25142
  delegationSessionId: sessionId
@@ -24812,7 +25378,7 @@ var ConnectedDroidSessionImpl = class {
24812
25378
  },
24813
25379
  addUserMessage: (params) => this.controller.addUserMessage(this.id, {
24814
25380
  ...params,
24815
- userMessageSource: "api" /* Api */
25381
+ userMessageSource: "sdk" /* Sdk */
24816
25382
  }),
24817
25383
  interruptSession: () => this.controller.interruptSession(this.id)
24818
25384
  };
@@ -24917,7 +25483,7 @@ var SessionsResourceImpl = class {
24917
25483
  sessionId,
24918
25484
  machineId: LOCAL_MACHINE_ID,
24919
25485
  token: this.apiKey,
24920
- tags: [...tags ?? [], SDK_TAG],
25486
+ tags: withSdkTag(tags),
24921
25487
  ...createSdkSessionAttribution(sessionId)
24922
25488
  });
24923
25489
  session.setMetadata(
@@ -25084,6 +25650,7 @@ async function connectToDaemon(options) {
25084
25650
  machineType: "local" /* Local */,
25085
25651
  url: options.url,
25086
25652
  clientType: "sdk",
25653
+ sdk: SDK_CLIENT_METADATA,
25087
25654
  getAccessToken,
25088
25655
  onAuthenticationError: options.onAuthenticationError,
25089
25656
  connectionTimeoutMs: connectionConfig.connectionTimeoutMs,
@@ -25195,7 +25762,9 @@ async function factoryFetch(path, apiKey, baseUrl, options) {
25195
25762
  const method = options?.method ?? "GET";
25196
25763
  const headers = {
25197
25764
  Authorization: `Bearer ${apiKey}`,
25198
- Accept: "application/json"
25765
+ Accept: "application/json",
25766
+ [FACTORY_CLIENT_HEADER]: "sdk" /* SDK */,
25767
+ [FACTORY_SDK_HEADER]: formatSdkClientMetadata(SDK_CLIENT_METADATA)
25199
25768
  };
25200
25769
  if (options?.body) {
25201
25770
  headers["Content-Type"] = "application/json";
@@ -25389,6 +25958,7 @@ export {
25389
25958
  SettingsLevel,
25390
25959
  SkillLocation,
25391
25960
  JSONRPC_VERSION,
25961
+ formatSdkClientMetadata,
25392
25962
  TraceContextMetaSchema,
25393
25963
  JsonRpcEnvelopeSchema,
25394
25964
  JsonRpcErrorSchema,
@@ -25679,6 +26249,8 @@ export {
25679
26249
  FactoryDroidMessageRole,
25680
26250
  ServerRequestHandlerType,
25681
26251
  SDK_VERSION,
26252
+ SDK_CLIENT_METADATA,
26253
+ SDK_REQUEST_ATTRIBUTION,
25682
26254
  SDK_TAG,
25683
26255
  JSONRPC_VERSION3,
25684
26256
  LEGACY_FACTORY_API_VERSION3 as LEGACY_FACTORY_API_VERSION2,
@@ -25702,6 +26274,7 @@ export {
25702
26274
  InvalidSessionCwdError,
25703
26275
  ProcessExitError,
25704
26276
  mergeSessionSettings,
26277
+ withSdkTag,
25705
26278
  unwrapResponseResult,
25706
26279
  SessionMetadataSchema,
25707
26280
  SessionStartDecompSessionTypeSchema,
@@ -25828,4 +26401,4 @@ react/cjs/react.development.js:
25828
26401
  * LICENSE file in the root directory of this source tree.
25829
26402
  *)
25830
26403
  */
25831
- //# sourceMappingURL=chunk-ROFAQUG4.mjs.map
26404
+ //# sourceMappingURL=chunk-KXDG5ZWS.mjs.map